cpp-oop-style · git:20260824.459b478 · 2026-08-24 · sha256 c4ca256929cd3d72
cpp-oop-style git:20260824.459b478A
Immutable. This exact content is served forever at /api/v1/blob/c4ca256929cd3d72.
---
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++". 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&`, `const T*` | East const: `T const &`, `T const *` |
## 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`.)
## 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).
## The `auto` idiom (AAA)
- **Almost Always Auto:** `auto x = Type{...}`, never `Type x(...)`. Forces
initialization and survives return-type changes.
- **Explicit cast over implicit:** `auto i = size_t{3};` not `auto i = 3;`.
- **In range-for: `auto const &` to read, `auto &` to modify.** Never bare
`auto` — it copies. For maps: `for (auto const &[k, v] : m)`.
- C++20 `auto` parameters are implicit templates: `auto square(auto const &x)`.
- **Dispatch on type with `concept`-constrained overloads**, the static twin of
virtual dispatch — never an `if constexpr (is_same_v<…>)` chain, which is the
compile-time form of the `getType()` / `enum`-switch anti-pattern. Reserve
`if constexpr` for capability gating (`requires { … }`) and variadic recursion.
(See `references/generics-compile-time.md`.)
## 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 neat, alert god-class tendency. When a class is piling too many methods and can be classified, 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.
- **`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/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.