- name
- cpp-oop-style
- description
- High-quality C++ OOP coding style (archibate / parallel101 lineage) that overrides sloppy AI-default C++. Use this skill WHENEVER writing, editing, refactoring, or reviewing C++ code (.cpp / .h / .hpp / .cc / .cxx), designing C++ classes, interfaces, APIs, or libraries, or when the user mentions C++ design, OOP, design patterns, dependency injection, RAII, or "clean / modern C++". Also use it for CMake-based C++ project setup, module targets, usage requirements, third-party dependency acquisition and integration, binary ABI compatibility, installation, packaging, and application deployment.
# archibate C++ OOP Style
A skill that makes you write C++ the way a senior systems engineer who loves
design patterns writes it — not the way an autocompleter does. When this skill
is loaded, it **overrides** your default C++ instincts.
## The one rule
> **"Abstract class, data class, or value type. Nothing else."**
Every type you introduce is one of these three kinds — never the muddy middle:
- **Abstract class** — *behavior only*. A pure-virtual interface with no data
members of its own; any injected collaborators live in its concrete `…Impl`,
not in the interface. Always has `virtual ~T() = default;`. This is the unit of
*polymorphism and dependency injection*.
- **Data class** — *data only*. A plain `struct` with public fields, built with
designated initializers. No business logic, no getters/setters wrapping plain
fields. This is the unit of *value passing and configuration*.
- **Value / resource type** — a concrete, value-semantic type that either owns a
resource (an RAII wrapper) or enforces one invariant (a strong type: `Money`,
`EmailAddress`, a math `Vector3`, a C-handle wrapper). It has a small, total
interface and behaves like a built-in — the *Regular* type. This is the one
concrete-class-with-methods that earns its keep.
Reject the muddy middle that models reach for by default: a concrete class that
mixes private fields, a grab-bag of public methods, *and* a scatter of free
helper functions — neither a clean interface, nor plain data, nor a focused value
type. That shape is the single biggest tell of AI-slop C++.
So a concrete class with methods is allowed only when it is **(a)** the
implementation of an abstract class (`struct FooImpl final : Foo`, defined
in a `.cpp`, never a header), or **(b)** a value/resource type as above.
Everything else is behavior behind an interface, or data in a struct.
## What you are overriding
| AI-slop default | This skill |
|---|---|
| Free functions `dep1DoX()`, `dep2DoX()` | One abstract interface `Dep`, injected |
| Concrete class with public mutable fields + methods | Abstract class (behavior) **or** data struct (data) |
| `Dog dog; dog.doThing(globalThing);` | Inject the collaborator: `dog.doThing(dep)` |
| `void f(string n, int a, int p, int addr)` | `void f(FooConfig const &cfg)` (designated init) |
| `new T` / `delete` / `new T[]` | `make_unique` / `make_shared` / `vector<T>` |
| `int parseInt()` returning `-1` on failure | `optional<int> parseInt()` |
| `enum Mode` + `switch` dispatch | inject a strategy / functor, or a state class |
| `pair<bool, It>` / `tuple<...>` returns | named result struct |
| `const T& x`, `const T* p` | East const: `T const &x`, `T const *p` |
## Named anti-patterns (real smells this overrides)
These are the concrete shapes that mark sloppy or dated C++ — name them and
refuse them:
- **God-base interface** — one abstract class fusing *data and behavior*, with a
pile of public mutable members (e.g. a node base every node both reads state
from and overrides). Split it: behavior → interface, state → data struct.
- **Global object + free functions** — a global instance poked by a scatter of
free helpers. Make it a class with a clear owner and inject it.
- **Stringly-typed API** — `setParam("mode", "fast")`, sockets/params keyed by
string. Use `enum class`, strong types, and named fields so the compiler
checks them. Relatedly, **fetch an abstract handle once** rather than re-passing
a string key on every call: `auto *dev = api->getDevice("CD"); dev->play();`,
not `api->playDevice("CD")` then `api->stopDevice("CD")`.
- **Sentinel returns** — `(size_t)-1`, `-1`, empty string, or null on failure.
Use `optional` / `expected` or a result struct (see `references/error-handling.md`).
## The canonical shape
```cpp
// Dep.h — interface only. Pure virtual. Lives in a small header.
struct MethodConfig {
Point position{};
float size{};
};
struct Dep {
virtual ~Dep() = default;
virtual std::string someQuery() const = 0;
virtual void someMethod(MethodConfig const &config) = 0;
};
// Animal.h
struct Animal {
virtual ~Animal() = default;
virtual void someInterface(Dep *dep) = 0; // collaborator injected, not owned
};
// Dog.h — concrete impl, declared minimally, defined in .cpp
struct Dog final : Animal {
void someInterface(Dep *dep) override;
private:
int somePrivate{};
};
// Dog.cpp
void Dog::someInterface(Dep *dep) {
auto answer = dep->someQuery(); // reuse, don't reimplement per concrete dep
// ...
}
// callSite.cpp — the composition root wires concrete to abstract
auto dog = Dog{};
auto dep1 = std::make_unique<Dep1>(someOptions);
dog.someInterface(dep1.get());
```
## Class design
**Virtual functions are a backbone of this style — reach for them.** They do
*two* distinct jobs, and both are worth an interface:
1. **Dispatch / dependency injection** — one shared caller works across subtypes
it doesn't know. This is what replaces *branching on a type tag*
(`switch (getType())`, `if (type == Dog)`) to pick behavior: let the vtable
dispatch, so adding a subtype touches no existing branch. Without it, every new
subtype copy-pastes the shared logic and one requirement change means editing N
files. The payoff is **open for extension, closed for modification** —
a new subtype, even one written later by another module or plugin, slots in
behind the interface without reopening any caller.
```cpp
void feed(Animal *a) { puts("feeding"); a->speak(); puts("done"); }
```
2. **Implementation hiding** — the interface lives in the header, the concrete
`…Impl` lives in the `.cpp`. This is worthwhile *even with a single
implementation*: a compile firewall (member types and heavy/third-party
headers stay out of your public header, callers don't recompile when the impl
changes), a clean ABI boundary, and a ready test seam. (See
"single-implementation interface" below.)
The only thing to avoid is the *empty* interface — a `virtual` that delivers
neither job: you already hold the concrete type, there is exactly one
implementation, and you gain no hiding, seam, or ABI benefit. That is pure
overhead. Everywhere a real seam exists — polymorphism **or** build/ABI/test —
prefer the interface.
**Escalate abstraction only as far as the duplication demands.** Lift a repeated
value to a variable, repeated logic to a function, a clump of arguments to a
struct, shared state-plus-behavior to a class, a fixed set of variants to an
`enum`, a fixed set of *types* to a `std::variant`, and an open set of behaviors
to a `virtual` interface — in that order. Don't jump to the interface when a
function would do. The real cost of copy-paste is not the typing — it is the typo
you later make in one rarely-run branch. When the type set is closed and known at
compile time, resolve it at compile time — a `variant` or `concept`-constrained
overloads (not an `if constexpr` type-switch; see
`references/generics-compile-time.md`).
**One interface, one responsibility.** Never mix concerns (e.g. IO *and*
computation) in one abstract class — it forces an N×M subclass explosion. Split
into independent interfaces and let a high-level function combine them:
```cpp
struct Inputer { virtual ~Inputer() = default; virtual std::optional<int> fetch() = 0; };
struct Reducer { virtual ~Reducer() = default; virtual int init() = 0; virtual int add(int, int) = 0; };
int reduce(Inputer *in, Reducer *r); // 2+2 classes, unlimited combinations
```
**Template Method — public non-virtual wrapper, protected virtual `do_xxx`.**
The public method owns the contract and supplies ergonomic overloads; subclasses
override only the raw `do_xxx`. (As in `std::pmr::memory_resource`.)
```cpp
struct Converter {
void process(std::string_view sv) { do_process(sv.data(), sv.size()); }
void process(char const *s) { do_process(s, std::strlen(s)); }
protected:
virtual void do_process(char const *s, size_t n) = 0;
};
```
**Strategy vs Template Method — which to pick.** Many independent behaviors on one
object → *Strategy*: hold pointers to injected strategy interfaces (a `Character`
with separate `move` and `attack` strategies). A single behavior that needs the
object's own members → *Template Method*: the base *is* the strategy, the
`virtual` reads its own fields (a `Weapon` whose `attack` uses its `damage` /
`range`). One axis of variation that owns no state → functor; several axes, or
state-carrying behavior → strategy objects.
**Thin virtual core, fat non-virtual API.** Put only primitives behind `virtual`
(`do_read`, `do_write`, `do_seek`); build the rich convenience API
(`getline`, `flush`) as non-virtual methods on top. Few virtuals, much reuse.
**Compose, don't multiply subclasses.**
- *Adapter*: wrap an interface, return the same interface, add one capability.
Adapters compose orthogonally instead of `N×M` subclasses.
- *State as class*: encode states as classes implementing a `State` interface,
not `enum + switch`. Adding a state touches no existing branch.
- *Component*: a `GameObject` holds `vector<unique_ptr<Component>>`. Use dynamic
composition for behavior, **never multiple inheritance.**
- *CRTP*: auto-implement boilerplate virtuals (`clone`, `accept`) once in a
`template <class D> struct Impl : Base` mixin instead of per subclass.
- *Visitor / double-dispatch*: when behavior depends on two types (or you'd
otherwise write `getType()` / `isEatable()` and switch on it), use
`accept`/`visit` so the compiler picks the overload — don't query a type tag.
- *Closed-set variant*: a fixed, known set of types → `std::variant` + `std::visit`
instead of a class hierarchy — value semantics, no heap or vtable. Use a
`virtual` interface instead when the set is open. (See
`references/generics-compile-time.md`.)
- *Flyweight*: when many objects share identical heavy data (a texture, a lookup
table), hoist it into a separate type held by a `shared_ptr`; keep only the
per-instance data (position, velocity) local. 1000 bullets, one shared sprite —
not 1000 texture copies. The owner's method just *forwards* to the shared object
(`sprite->draw(position)`) — that delegation is the *proxy* idiom.
**Interface/implementation split (header hygiene).** Put the pure-virtual
interface in a small header; keep the concrete `…Impl final` entirely in the
`.cpp`. Hand back the interface through a **factory**, so callers never see — or
`#include` — the concrete type:
```cpp
// Foo.h
struct Foo { virtual ~Foo() = default; virtual void run() = 0; };
std::unique_ptr<Foo> createFoo(FooConfig const &cfg); // factory returns the interface
```
This is also how you select backends: define the factory once per backend
directory and let the build system link exactly one. Swapping an implementation
(real vendor SDK ↔ a fake for tests/replay) becomes a build-variable change, not
a code change — the test double is just another implementation behind the seam.
**A single-implementation interface is justified — for hiding, not dispatch.**
Even when only one `…Impl` will ever exist, the compile-firewall / ABI / test-seam
payoff of point 2 still earns the interface — the deliberate exception to "don't
over-abstract." The public header carries only the interface and a factory; the
sole `…Impl` and its heavy headers stay in the `.cpp`:
```cpp
// Widget.h — interface + factory are the whole public surface
struct Widget {
virtual ~Widget() = default;
virtual void draw() = 0;
};
std::unique_ptr<Widget> makeWidget(WidgetConfig const &cfg);
// Widget.cpp — the lone impl and its <heavy/thirdparty.h> are hidden here
struct WidgetImpl final : Widget {
heavy::thirdparty::Object object;
explicit WidgetImpl(WidgetConfig const &cfg) { /* ... */ }
void draw() override { /* ... */ }
};
std::unique_ptr<Widget> makeWidget(WidgetConfig const &cfg) {
return std::make_unique<WidgetImpl>(cfg);
}
```
Prefer this over classic value-semantic PIMPL since it allows a test fake or a
second backend later; plain PIMPL gives *only* the compile firewall, no seam.
**Command/callback pairs (Api / Spi).** For a subsystem with inversion of
control, split the two directions into two interfaces: an **`Api`** (the
*application programming interface* — commands you call *into* the subsystem) and
an **`Spi`** (the *service provider interface* — events the subsystem calls *back*
out to you). The owner implements the `Spi` and holds the `Api`; wire the two with
`api->setSpi(this)`.
```cpp
struct PlayerSpi { // you implement — called back on events
virtual ~PlayerSpi() = default;
virtual void onTrackEnded() = 0;
};
struct PlayerApi { // you call in — commands
virtual ~PlayerApi() = default;
virtual void setSpi(PlayerSpi *spi) = 0;
virtual void play(Track const &t) = 0;
};
struct App final : PlayerSpi { // owner: implements Spi, holds Api
explicit App(PlayerApi *api) : api(api) { api->setSpi(this); }
void onTrackEnded() override { api->play(next()); } // reacts to the callback
PlayerApi *api;
};
```
**Singleton — encapsulate the one instance, never a bare global.** For a genuinely
process-wide subsystem, hide the constructor, delete copy/move, and hand out the
instance through one accessor — define it in the `.cpp` like any other method:
```cpp
// Game.h
struct Game {
void update();
static Game &instance(); // the sole accessor
Game(Game &&) = delete;
private:
Game();
};
// Game.cpp
Game &Game::instance() { static Game inst; return inst; } // lazy, thread-safe (C++11)
```
A header form — a header-only util, or the generic
`template <class T> T &singleton() { static T inst; return inst; }` — must be
`inline`, not `static`, and gets a separate copy per Windows DLL. A singleton is
still global state: prefer injection through the composition root, and reserve it
for subsystems that are truly one-per-process.
## Dependency injection
- **Inject abstractions into high-level functions, never concrete types.** The
caller chooses the implementation; the callee depends only on the interface.
- **Inject a factory, not a product, when the callee must create many.** Give a
`Gun` whose `virtual unique_ptr<Bullet> shoot()` the callee calls repeatedly —
not a single pre-made `Bullet`.
- **A single composition root does all the wiring.** One `main.cpp` (or one
setup function) calls the factories and injects via constructor args or
setters. No globals reach across modules; production vs test differ only by
which factories the root calls.
- **Collaborators are borrowed, not owned.** Pass dependencies as raw interface
pointers (`Dep *`) or references; the injectee never owns its collaborators.
Ownership lives in the composition root. (See `references/ownership-lifetime.md`.)
## CMake-first project structure
- **Make the target graph mirror the module graph.** Give each architectural
module its own directory, `CMakeLists.txt`, and library target; let executable
targets be composition roots that link those modules.
- **Keep public structure explicit.** Put exported headers under
`include/<module>/`, implementations under `src/`, include them as
`<module/Foo.h>`, and use the module name as the C++ namespace.
- **Attach requirements to the target that owns them.** Sources, include paths,
definitions, options, and dependencies use `target_*`; choose `PRIVATE`,
`PUBLIC`, or `INTERFACE` from whether consumers need the requirement.
- **Name repeated configuration profiles with CMake Presets.** When developers
repeatedly choose among several options, build types, or toolchains, commit a
`CMakePresets.json` so configure, build, and test use short named profiles with
separate build trees instead of reconstructed `-D...` command lines.
- **Prefer an `OBJECT` library for an internal module folded into final products
View on GitHub