cpp-oop-style · git:20260904.548d7fe · 2026-09-04 · sha256 40f225ae5bc101ec
cpp-oop-style git:20260904.548d7feA
Immutable. This exact content is served forever at /api/v1/blob/40f225ae5bc101ec.
---
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-first C++ project layout, module targets, usage
requirements, third-party dependency acquisition and integration, binary ABI
compatibility, installation, packaging, and application deployment. Apply it
even when the user does not explicitly ask for a style: the default way models
write C++ leans on free functions, public mutable state,
raw new/delete, sentinel return codes, and long loose parameter lists — this
skill replaces all of that with abstract-class-or-data-class design,
dependency injection, type-rich APIs, value-based error handling, and RAII
ownership.
---
# 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
in one build tree.** Multiple in-tree apps, tests, or probes do not require an
archive. Use `STATIC` or `SHARED` when the library is itself a deliberate
archive, runtime, ABI, installation, or deployment boundary.
- **Normalize each third-party dependency to one CMake target.** Prefer an
upstream target whether its source is vendored with `add_subdirectory` or
discovered as an installed package. Wrap header-only trees, pkg-config data,
legacy variables, and raw binary SDKs behind a target instead of scattering
include paths and flags across consumers.
- **Give each logical dependency one provider and version in the final graph.**
Resolve diamonds at the composition root; do not let two parents silently
embed incompatible copies of the same library.
- **Choose the delivery contract before adding packaging machinery.** Public
libraries and geek-oriented CLI tools get a textbook install target and source
archive; end-user applications get a dedicated artifact pipeline; pybind11
extensions are installed into a Python wheel staging tree.
Read `references/cmake-first-projects.md` before creating or restructuring a
CMake C++ project, changing module targets, or deciding dependency visibility.
For acquiring, building, finding, vendoring, or wrapping a third-party library,
or for diagnosing a binary dependency ABI mismatch, read
`references/dependencies/router.md` first.
For any install, package, release archive, portable bundle, AppImage, native
installer, or Python-extension distribution task, read
`references/deployment/router.md` first. It routes further by deliverable type
and distribution scope.
## Type-rich data classes
Make illegal states unrepresentable and make call sites self-documenting. The
compiler is your reviewer.
- **Bundle ≥3 related params into a named struct with designated init.** Names
beat positions; adding a defaulted field breaks zero callers.
`void foo(FooConfig const &cfg);` then `foo({.name = "x", .age = 24});`
- **Return a named struct, never `pair`/`tuple`.** `result.success` not
`result.first`.
- **`optional<T>` for nullable returns** — never a sentinel like `-1` or a
nullable raw pointer. (Error handling: `references/error-handling.md`.)
- **Don't reflexively wrap fields in `optional<T>`** — reserve it for genuinely
sometimes-absent data; on an always-present field it just sprays null-checks. A
real either/or is a `std::variant` or distinct types, not a nullable.
- **`enum class` for flags/states** — blocks implicit `int` conversion and
argument-order bugs.
- **Strong types for primitives that should not interconvert.** Wrap in a
one-member struct or `enum class FileHandle : int {}` so `read(fd, …)` can't
silently take the wrong `int`.
- **`std::span<T>` / `string_view` for non-owning buffer/string params** —
length travels with the data, no `ptr,len` mismatch.
- **`std::chrono` for time**, never raw integers — `time_point + time_point`
becomes a compile error instead of a 54-year sleep.
- **Plain data is a `struct` with public fields**, constructed by aggregate
initialization — `Foo{a, b}` or designated `Foo{.x = a, .y = b}` — with no
hand-written constructor and no encapsulation ceremony.
- **Getters/setters earn their place only to guard an invariant** — inside a
value/resource type. Independent fields stay public (a `Point`'s `.x`/`.y` need
no `getX`/`setX`); fields coupled by an invariant hide behind hook methods with
mutation banned (a `vector` exposes `size()`/`resize()` and a read-only `data()`
because resizing must reallocate).
- **Name constructors by intent — use named static factories** when variants
differ in meaning, not signature (`Cake::makeChoco()` / `Cake::makeMoca()`,
not `Cake(double)` vs `Cake(int)`).
## Naming & layout
- **No `m_` prefix, no trailing-underscore on members.** Members are bare names.
- **Trailing underscore only on a ctor/setter param that shadows a member:**
`void setX(double x_) { x = x_; }`.
- Types `PascalCase`; methods & members `camelCase`; constants `kPascalCase`;
`enum class : uint8_t` with explicit underlying type.
- Predicate methods read as intent: `shouldRetry()`, `canFlush()`.
- **One concept per header**, kept small. `#pragma once`, never include guards.
- **Forward-declare in headers, `#include` in the `.cpp`** to cut compile
coupling.
- **East const everywhere:** `T const &`, `T const *` — const binds to what
precedes it, which reads consistently right-to-left.
- **Always `struct`, never the `class` keyword** — even for encapsulated types.
Open an explicit `private:` / `protected:` section when you need encapsulation
(`struct Game { void play(); private: Game(); };`). The keyword carries nothing
the access labels don't, and defaulting to `struct` keeps each type's public
surface first and visible.
- In headers, share definitions with `inline`, never `static` (which silently
duplicates per translation unit).
## Use `auto` wisely in local variables
For an ordinary local value, use one of two declaration forms (adding `const`
according to the next section):
```cpp
auto x = rhs; // deduced form: the type is locally evident
Type x{rhs}; // named receiving form: the type adds missing semantics
```
Use the deduced form when the RHS names the type, the producer and its type are
evident within the same function body, or the exact type is lengthy, unnamed, or
deliberately an implementation detail:
```cpp
auto const i = std::size_t{1};
auto worker = std::make_unique<Worker>(config);
auto values = std::vector<int>(3); // `()` intentionally selects the count ctor
```
Use the named receiving form when a reader cannot recover the type from the
current function without inspecting a callee, and the type is important to the
meaning of the code:
```cpp
State const state{machine.state()};
```
The named form receives an existing expression. When supplying constructor
arguments, put the constructed type on the RHS: `auto dog = Dog{"George", 10};`.
Do not write `Type x = rhs`, `Type x(rhs)`, or an uninitialized `Type x`; those
forms can hide conversions, misuse parentheses, or leave built-in values
indeterminate. Braces make narrowing conversions ill-formed:
```cpp
std::size_t const count{fetchCount()};
// int count = fetchCount(); // implicit narrowing
// int count(fetchCount()); // implicit narrowing
int const count{static_cast<int>(fetchCount())}; // only after a range check
```
Do not optimize for silently surviving return-type changes. A named receiving
type should make narrowing API drift a compile error instead of an implicit
conversion. Braces do not require exact type equality; use strong types when
even otherwise-valid conversions must be rejected.
`auto` deduction does not preserve top-level `const` or references. In
range-for, use `auto const &` to read and `auto &` to modify; bare `auto` copies.
For maps: `for (auto const &[key, value] : map)`.
## The `const` idiom
- **Almost Always Const:** write `auto const value = makeValue();` unless the
binding must later be reassigned or moved from. Mutation should be deliberate
and visible at the declaration.
- **Declare every pass-by-value parameter `const`:** spell it `T const`,
`std::span<T> const`, or `T const *const`. Top-level `const` freezes the local
parameter binding; element or pointee constness remains a separate choice.
- **Prefer new `const` variables over of reuse:** declare new local variables
for logically different variable instead of re-assigning existing ones. Only
reuse when a loop or iteration involves iterative update of a same variable.
- **Mark every observation-only member function `const`.** A query may not
mutate the object's observable value; require the same qualifier on interface
declarations and overrides.
- **Expose read-only access with a const view:** `T const &`, `T const *`,
`std::span<T const>`, or `std::string_view`. Return mutable access only when
mutation is an explicit part of the API contract.
- **Leave a local non-const when ownership must move from it.** `const` blocks
moving from move-only values and may turn an intended move into a copy; never
return `T const` by value for the same reason.
- **Reserve `mutable` only for logical constness**, such as a cache or mutex that
does not change the observable value. Never for hiding ordinary state changes.
## Signature clarity
Function signatures should be self-explained. An interface should convey its expected behavior from its declaration. A function should explain its purpose solely from name and types without ambiguity.
**Why:** when writing code, reading the header can explains the expected behavior for most trivial functions.
Use a name telling full story e.g. `Process::isRunning()` instead of `Process::check()`, unless the class already locks the context e.g. `OutOfOrderChecker::check()`.
Avoid ambiguious function and class names, rename them immediately once you flag one.
**Why:** a confusing interface name may confuse future agents to risk misuse them.
When there are ambiguity of generic type in argument, define and use type-rich classes `sleep(Duration const &)`, `findByName(Name const &)`.
When there are multiple argument whose order and meaning are ambiguious, use `fill(Rect const &)` and invoke with `Rect{...}`.
**Why:** saves future agent from drifting type semantics during refactor.
## Duty class
Keep class interface small and orthogonal. Alert god-class tendency. When a class is piling too many methods and can be classified into orthogonal categories, consider breakdown heavy duty cluster into duty class.
E.g. `std::unique_ptr<Painter> Canvas::getPainter()` + `Painter::fill(Path const &, Brush const &)` + `Painter::stroke(Path const &, Pen const &)`. Here `Painter` can be another abstract class, and `Canvas` implements `Paintable` which requires `Paintable::getPainter()`. `Canvas.cpp` can implement that as `CanvasPainter` privately using anonymous namespace (a typical implementation can holds a `Canvas *` pointer). This keeps the `Canvas` interface stay focused, also reserve for future `Paintable` implementations.
**Why:** programmers and LLMs works better when knowledge is progressively disclosed. Reading a god-class floods context by side-cars unrelevant to the goal. So keep interface small and hierarchy to avoid dilution.
## Don't repeat yourself
When there are more than 2~3 paths sharing common pattern or concept: extract into abstracted class. E.g. `Path` for `Line`, `Arc`, `Bezier`; `Brush` for `Color`, `Gradient`; saves `fill()` from combination hell.
Two approaches to abstraction:
- Potentially vast expansion in future -> **Dynamic polymorphism**: `Path` as abstract class; pointer semantics; pass as `Path const &` (or `Path *` if mutable); return and store as `std::unique_ptr<Path>`.
- Fixed types, likely won't expand -> **Static polymorphism**: `Brush` as a data-class wrapped `std::variant`; value semantics; pass as `Brush const &`; return and store as `Brush`.
Avoid using function overload and templates for polymorphism unless the context is metaprogramming or performance.
## Boolean expression style
Prefer the C++ alternative operator tokens `not`, `and`, and `or` in
human-written boolean expressions. They are core-language keywords with exactly
the same semantics and precedence as `!`, `&&`, and `||`, but they are harder to
miss while scanning:
```cpp
if (not isReady() or (isExpired() and canRetry())) {
return false;
}
```
- Parenthesize mixed `and` / `or` expressions even when precedence already gives
the intended result.
- Prefer a positive named predicate over a dense negation; introduce
`isUnavailable()` when it communicates a recurring domain concept better than
`not isAvailable()`.
- Keep `!=` and bitwise operators symbolic. Do not generalize this rule to
uncommon spellings such as `not_eq`, `bitand`, or `xor`.
- This rule is for boolean expressions, not rvalue references (`T &&`) or
declarations such as `operator&&`. Match third-party and generated code rather
than rewriting it solely for house style.
## Prefer brace initialization
Prefer direct-list initialization (`{}`) over direct initialization (`()`) when
the two forms select the same constructor:
```cpp
struct Dog {
explicit Dog(std::string name, std::int32_t age);
};
auto dog = Dog{"George", 10}; // NEVER: Dog dog("George", 10);
```
This is **list initialization**, not an "aggregate constructor." Aggregate
initialization is only the constructor-free data-class case such as
`Point{.x = 1, .y = 2}`. `Dog` above has a user-declared constructor and is not
an aggregate.
Use `()` when braces intentionally select an `initializer_list` overload with
different semantics. `std::vector` is the canonical example:
```cpp
auto oneValue = std::vector<std::int32_t>{3}; // one element: {3}
auto threeZeros = std::vector<std::int32_t>(3); // three elements: {0, 0, 0}
auto twoValues = std::vector<std::int32_t>{3, 42}; // {3, 42}
auto threeValues = std::vector<std::int32_t>(3, 42); // {42, 42, 42}
```
An implicit constructor permits copy-list initialization at a call site. This is
not aggregate initialization either:
```cpp
struct Dog {
Dog(std::string const &name, int age);
};
void showDog(Dog const &dog);
showDog({"George", 10});
auto dogs = std::vector<Dog>();
dogs.push_back({"George", 10});
```
## When to use `explicit` constructor
- **Default to `explicit`** for every converting constructor, including
multi-argument constructors used through `{...}`.
- Allow implicit conversion only when the source and destination are genuinely
substitutable values and the conversion is unsurprising and lossless, such as
a UTF-8 string literal becoming an owning `std::string`.
- Different semantics require `explicit`: a count is not a container, a raw
handle is not an owning resource, and an integer is not an age merely because
their representation matches.
- When construction modes differ by intent, use named factories rather than
constructor overloads: `Angle::fromDegrees(x)` and `Angle::fromRadians(x)`.
```cpp
struct BigInt {
BigInt(std::int32_t value); // exact, lossless value-domain extension
};
struct Dog {
explicit Dog(std::string const &name);
};
void sendMsg(std::string const &msg);
void showBigInt(BigInt const &big);
void showDog(Dog const &dog);
void usage() {
sendMsg("hello");
showBigInt(42);
showDog(Dog{"George"});
}
```
## C++ cast ladder
Pick casts by the semantic conversion being requested. For arithmetic values,
"up-cast" and "down-cast" are misleading: signedness, range, precision, and the
runtime value all matter.
- **Known-safe constant → braces.** List initialization rejects narrowing at
compile time: `auto channel = std::uint8_t{42};` is valid while
`std::uint8_t{300}` is ill-formed.
- **Runtime integral conversion → check, then `static_cast`.** In C++20 use
`std::in_range`; in C++17 compare against `numeric_limits` with signedness
handled explicitly:
```cpp
std::optional<std::size_t> toSize(std::int32_t value) {
if (not std::in_range<std::size_t>(value)) return std::nullopt;
return static_cast<std::size_t>(value);
}
```
- **Floating-point → integer → define the policy first.** Reject non-finite and
out-of-range values, then choose truncation, floor, ceil, or rounding before
the final `static_cast`. A naked cast silently bakes in truncation and is
undefined when the finite result is outside the destination range.
- **Representation conversion → `std::bit_cast`** only between equally sized,
trivially-copyable types. In C++17 use `std::memcpy` with the same static
assertions. This is not numeric conversion.
For a polymorphic `Dog : Animal` hierarchy:
- Derived-to-base pointer/reference conversion is implicit. Returning
`unique_ptr<Animal>` from a factory deliberately hides `Dog`.
- Prefer virtual dispatch over recovering the concrete type. When a boundary
genuinely requires checked base-to-derived conversion, `dynamic_cast<Dog *>(p)`
returns `nullptr` on mismatch; `dynamic_cast<Dog &>(r)` throws `std::bad_cast`.
- Use `static_cast<Dog *>(p)` only when a nearby invariant proves the dynamic
type. Assert that invariant where it is established; a wrong unchecked
downcast has undefined behavior.
Avoid `reinterpret_cast`. Its legitimate uses are narrow low-level boundaries,
such as the implementation-required pointer/`uintptr_t` round trip. Converting
an object pointer to `void *` is implicit; converting a byte buffer to a packed
struct is not a safe zero-copy parser because alignment, lifetime, and aliasing
still apply. Copy bytes with `memcpy`/`bit_cast`, then validate the fields.
Avoid `const_cast`. It is tolerable only when adapting a legacy API whose
signature incorrectly omits `const` and which is known not to write. Modifying an
object that was originally defined `const` is undefined behavior.
Ban C-style casts `(T)x`: they can silently combine `static_cast`,
`const_cast`, and `reinterpret_cast`. Use braces, a named C++ cast, or a
domain-specific conversion function that makes validation visible.
## C++ arithmetic types
Choose an integer type from the value's meaning, not from a blanket ban:
- Use `std::int8_t` / `std::uint32_t` and friends when an exact width is part of
a wire format, file layout, ABI, SIMD lane, or hardware register. Exact-width
typedefs are optional on platforms that cannot provide that width.
- Use `int` for ordinary small signed arithmetic when no exact width is part of
the contract. Do not serialize it or expose its layout as an ABI promise.
- Use a container's `size_type` (usually `std::size_t`) for sizes and indices
that must interoperate with that container. Use `std::ptrdiff_t` for signed
distances and subtraction. Do not mix signed and unsigned values casually.
- Use `std::uintptr_t` only when the implementation provides it and an integer
must round-trip an object pointer. It is not a generic "native integer."
- Avoid bare `long` in portable layouts: it differs between LP64 and LLP64.
Use `float`, `double`, or `long double` according to the required precision,
range, ABI, and measured performance. Append `f` to a floating literal intended
to be `float`, such as `3.14f`; do not rely on an implicit `double` conversion.
## Add assert when you made assumption
Use `static_assert` for compile-time properties and `assert` for internal runtime
invariants. Put the check next to the assumption it protects:
```cpp
auto const b = someInt();
auto const a = someInt();
auto const diff = b - a;
// Keep a future return-type change from making `diff < 0` always false.
static_assert(std::is_signed_v<decltype(diff)>);
if (diff < 0) {
return false;
}
```
```cpp
auto const v = internalAlgorithm();
assert(not v.empty());
return v.back() - v.front();
```
`assert` disappears when `NDEBUG` is defined. Never use it to validate external
input or report a recoverable failure:
```cpp
auto const v = fetchFromInternet();
if (v.empty()) return std::nullopt;
return v.back() - v.front();
```
**Construction as validation:** put one invariant in a value type so downstream
code cannot receive an invalid value. Encapsulation earns its place by making
the illegal state unrepresentable.
```cpp
struct Age {
static std::optional<Age> fromYears(std::int32_t value_) noexcept {
if (value_ < 0 or value_ > 130) return std::nullopt;
return Age{value_};
}
std::int32_t value() const noexcept { return raw; }
private:
explicit Age(std::int32_t raw_) noexcept : raw(raw_) {}
std::int32_t raw;
};
struct UserConfig {
Email email;
UserName name;
Age age;
};
auto const age = Age::fromYears(inputAge);
if (not age) return false;
registry->registerUser(UserConfig{.email = email, .name = name, .age = *age});
return true;
```
Return C++23 `std::expected` when the caller needs an error reason. In C++20/17,
use the project's `expected` backport, a named result struct, or `optional` when
no error detail is needed.
`UserConfig` remains an aggregate data class because each field is independently
valid. If validity depends on a relationship among several fields, replace it
with one composite value type and a validating named factory; a struct with a
validating constructor is no longer the skill's "data only" data class.
## Function size discipline
**Function discipline.** Decompose programs into named, single-responsibility
functions — don't pile logic into `main`, and don't fuse unrelated jobs (a `sum`
that also prints; let the caller decide what to do with the result). Prefer
early-return guard clauses over deep nesting, and keep each function within a
screenful — Linus's rule of thumb: ≤3 levels of nesting, ≤24 lines, ≤80 columns.
## Pragmatics — when to dial it back
This is a style for code that must live and change. Don't weaponize it:
- **Don't pre-abstract.** A one-off internal helper does not need an interface.
Add the *dispatch* seam when a second implementation actually appears (or is
imminent). This is about polymorphism only — a single-implementation interface
for a compile firewall, ABI boundary, or test seam is still justified (see
"single-implementation interface" under Class design).
- **Hot paths prefer a template `Func` over `std::function`/virtual** for
zero-overhead dispatch. (See `references/functors-callbacks.md`.) In a *measured*
inner loop it is even fine to drop OOP entirely — raw intrinsics, free
functions, value-semantic SIMD wrappers — provided every such kernel is paired
with a reference-checked test and a benchmark. Performance you can't measure is
not a reason to abandon the style. (See `$cpp-hpc-optimization`.)
- **`shared_ptr` vs `unique_ptr`:** prefer a single clear owner (`unique_ptr`,
or a process-lifetime raw owning pointer for singletons); reach for `shared_ptr`
only when ownership is genuinely shared.
## Exemplar libraries — good API to imitate
When unsure what a well-designed API looks like, study these. Each is a clean
demonstration of one principle:
| Library | Principle it demonstrates |
|---|---|
| **fmt** / `std::format` | type-rich, compile-time-checked format API; no unsafe varargs |
| **ranges-v3** / `std::ranges` | composable lazy adaptors over concrete containers |
| **magic_enum** | type-safe enum reflection without macros or codegen |
| **nlohmann-json** | RAII ownership and type-deduced `get<T>()` |
| **tl::expected** / `std::expected` | value-based error propagation |
| **structopt** | struct-as-API — a plain data class drives the interface |
## What not to imitate
Fine to *use*; wrong to *copy the style of*:
- **poco** — raw `new`/`delete` throughout, Java-style OOP, no value semantics.
- **rapidjson / jsoncpp** — SAX template maze / weakly-typed `Value` tree.
- **tinyxml2, legacy OpenCV C API, stb** — raw-pointer, pre-RAII style.
Qt is a different case: it is *excellent* **classic** OOP — object-tree ownership,
signals/slots, `QObject` parenting. Its `m_` members, raw `new`, and
parent-owns-child idioms are deliberate and correct *for that paradigm*. Keep
them inside Qt code; just don't carry them into value-semantic modern C++, where
this skill's conventions apply.
## Compiler hygiene
Let the compiler enforce the style — most rules above become hard errors instead
of review comments. Build with:
```
-Wall -Wextra -Weffc++
-Werror=return-type -Werror=uninitialized
-Werror=suggest-override # every override marked `override`
-Wzero-as-null-pointer-constant # `nullptr`, never `0` / `NULL`
-Wold-style-cast # named casts only, never `(T)x`
-Werror=vla # `std::vector` / `std::array`, never VLAs
-Wnon-virtual-dtor -Wdelete-non-virtual-dtor
-Wconversion -Wsign-compare # no silent narrowing
-Werror=unused-result # don't ignore a [[nodiscard]] result
```
Add `-D_GLIBCXX_DEBUG` in development builds to catch iterator/bounds misuse at
runtime (every linked translation unit must match).
## References
You MUST proactively load these when the task touches their area:
- `references/decoupled-modules.md` — definite computation vs tacit I/O or GUI
boundaries, interface seams, agent-operable harnesses, and integration gates.
Load me before decomposing a new C++ project or multi-module architecture.
- `references/cmake-first-projects.md` — course-derived CMake-first directory
layout, target kinds, usage requirements, named configuration presets, source
discovery, third-party dependencies, and embeddable subprojects. Load me
before creating or restructuring CMake C++ targets or dependency wiring.
- `references/dependencies/router.md` — third-party dependency router by source
control, package metadata, graph ownership, and ABI risk. Load me first for
vendoring, package discovery, manual library integration, binary SDKs,
dependency diamonds, or C++ ABI mismatches; then follow the matching leaf.
- `references/deployment/router.md` — deployment router by deliverable type,
audience, and portability scope. Load me first for installation, packaging,
release archives, application artifacts, native installers, or Python wheels;
then follow only the matching child routes.
- `references/debug-instrumentation.md` — discriminating state capture, mature
logging backends, stable instrument keys, JSONL, and bounded hot-path probes.
Load me before adding or structuring temporary diagnostic logs.
- `references/debug-harnesses.md` — minimal harnesses, application or GDB REPLs,
durable developer surfaces, customer diagnostics, live calibration, and fast
diagnostic builds. Load me when debugging needs controllable execution or when
designing persistent development and support controls.
- `references/ownership-lifetime.md` — no raw `new`, smart pointers vs `vector`,
references vs pointers, RAII for C resources, the rule of five, dangling
temporaries. Load me before smart pointers, or resource management design.
- `references/functors-callbacks.md` — template `Func` vs `std::function`,
lambdas over `std::bind`, capture lifetime, closures as structs. Load me on
function-programming context.
- `references/error-handling.md` — recoverable vs unrecoverable, `optional` /
`expected`, `[[noreturn]]`, the result-struct / error-sink / bool+log fallbacks.
Load me before I/O interface, business logic, error handling, or third-party error
code wrapper.
- `references/wrapping-c-resources.md` — RAII wrappers for opaque C handles:
move-only handle template, `error_category`, check-on-assign with
`source_location`, builders, scope-guard binds. Load me before integrating
third-party libraries (e.g. OpenGL, CUDA) or manage OS resources with C handles.
- `references/generics-compile-time.md` — compile-time dispatch via
`concept`-constrained overloads (not type-switching), `if constexpr` capability
gating, `std::variant` + `std::visit` closed-set polymorphism, perfect forwarding.
Load me when static polymorphism could surpass dynamic polymorphism.
- `references/type-erasure.md` — subtype hiding vs non-intrusive erasure,
choosing standard wrappers, and a C++17-compatible interface/model wrapper.
Load me when wrapping type erased interface.
- `references/text-encoding.md` — character/code-unit types, UTF-8 storage,
filesystem paths, and Qt/platform text boundaries. Load me when handling
Unicode, encodings, local paths, or text across library/platform boundaries.
- `$cpp-hpc-optimization` — evidence-driven data-oriented layout, numerics,
cache/locality, SIMD, parallelism, and hot-path polymorphism. Load it before
designing or optimizing a high-throughput kernel or data structure; keep this
skill's abstract boundaries on the cold/control side and dispatch into
homogeneous data batches on the measured hot side. For hot closed-set
polymorphism, prefer dense per-concrete-type pools such as
`vector<Dog>` plus `vector<Cat>` over per-element base pointers or variants;
keep any virtual dispatch at the pool/batch boundary.
- `references/sources.md` — original parallel101 material, exemplar code, and
further-study tools. Load me when verifying provenance or rationale, or when
looking for deeper examples behind a rule.