Skip to main content

cpp-oop-style

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.

Source facts

Repository
archibate/agent-skills
Last source activity
October 4, 2026 at 14:47
Detected SKILL.md language
English
Stars
16
Forks
2

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
24 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub