Modern C++ (C++17/20/23) coding standard grounded in the public C++ Core Guidelines. Enforces resource safety (RAII), immutability by default, strong static typing, value semantics, and intent-revealing interfaces. Covers ownership and smart-pointer discipline, the Rule of Zero / Rule of Five, const-correctness, scoped enums, exception-safety, concept-constrained templates, concurrency locking discipline, header hygiene, and naming. Use when authoring, reviewing, or refactoring C++ — when deciding between raw and smart pointers, choosing enum vs enum class, sizing a class's special members, passing parameters, constraining a template, or locking a shared resource. Not for C-only legacy that cannot adopt modern features, or bare-metal contexts where a specific guideline conflicts with hardware.
Installation
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Modern C++ (C++17/20/23) coding standard grounded in the public C++ Core Guidelines. Enforces resource safety (RAII), immutability by default, strong static typing, value semantics, and intent-revealing interfaces. Covers ownership and smart-pointer discipline, the Rule of Zero / Rule of Five, const-correctness, scoped enums, exception-safety, concept-constrained templates, concurrency locking discipline, header hygiene, and naming. Use when authoring, reviewing, or refactoring C++ — when deciding between raw and smart pointers, choosing enum vs enum class, sizing a class's special members, passing parameters, constraining a template, or locking a shared resource. Not for C-only legacy that cannot adopt modern features, or bare-metal contexts where a specific guideline conflicts with hardware.
metadata
{"activation_triggers":["authoring, reviewing, or refactoring C++ code","deciding raw vs smart pointers or ownership transfer","sizing a class's special members (Rule of Zero / Rule of Five)","constraining a template (concepts) or choosing enum vs enum class","locking a shared resource (scoped_lock / lock_guard discipline)","header hygiene, include discipline, or naming in C++"],"paths":["**/*.cpp","**/*.cc","**/*.cxx","**/*.hpp","**/*.hh","**/*.ipp","**/*.h"]}
A working standard for modern C++ (C++17 and newer) built on the public
C++ Core Guidelines.
Rule identifiers below (for example R.20, C.21, ES.25) are references
into that public document — consult it for the full rationale. The examples
here are original and illustrate how the standard applies in review.
When to Activate
Reach for this skill when you are:
Writing new C++ — a class, free function, template, or module.
Reviewing or refactoring existing C++ for safety and clarity.
Deciding between two language features (raw pointer vs smart pointer,
enum vs enum class, output parameter vs returned struct).
Sizing a class's special member functions (destructor, copy, move).
Constraining a template or picking a parameter-passing convention.
Locking a resource shared across threads.
Skip it for C-only legacy code that cannot adopt modern C++, and adapt it
selectively on bare-metal or freestanding targets where a specific rule
conflicts with a hardware constraint (document the deviation).
Six Principles That Drive Every Rule
Almost every specific rule is a consequence of one of these. When a
concrete rule is unclear, fall back to the principle.
Bind every resource to an object's lifetime (RAII). Acquisition is
construction; release is destruction. No manual paired cleanup.
(P.8, R.1, E.6)
Immutable by default. Start from const / constexpr; make a thing
mutable only when you have a reason. (P.10, Con.1, ES.25)
Let the type system catch the bug. Prefer compile-time errors to
run-time checks; encode units and invariants in types. (P.4, P.5,
I.4)
Say what you mean. Names, signatures, and concepts should carry
intent without a comment. (P.1, P.3, F.1)
Keep it small. A function does one thing; a scope stays tight; a
class exposes the minimum. (F.2, F.3, ES.5, C.9)
Value semantics first. Return by value, hold scoped objects, and
reserve pointers for non-owning observation. (C.10, R.3, F.20)
Interfaces (P., I.)
Make an interface tell the caller exactly what it needs and who owns what.
An honest signature removes a class of misuse before the first call.
Prefer — a strongly typed, unit-carrying interface that cannot be
called with a bare number:
// A distance is not a double. The type prevents mixing units.structMeters { double value; };
structSeconds { double value; };
Meters braking_distance(Meters speed_limit, Seconds reaction);
Avoid — a weak signature with unclear ownership and unclear units, and
a mutable global that any translation unit can perturb (I.2, I.11):
doublebraking_distance(double a, double b); // units? order? meaning?int g_request_count = 0; // non-const global — avoid
Guidance worth keeping in muscle memory: interfaces are explicit (I.1),
precisely typed (I.4), never transfer ownership through a raw pointer or
reference (I.11), and keep the argument count low — group related
arguments into a struct once you pass three or four (I.23).
Functions (F.*)
A function performs one logical operation (F.2) and stays short enough to
read at a glance (F.3). Two decisions recur:
Parameter passing (F.16). Pass cheap-to-copy types by value; pass
expensive types by const&; take a by-value "sink" parameter when the
function will store a moved copy.
voidset_retry_limit(int limit); // cheap: by valuedoublechecksum(const std::vector<std::byte>& b);// expensive: by const&voidadopt_name(std::string name){ // sink: by value, then move
name_ = std::move(name);
}
Returning results (F.20, F.21). Return values, not output
parameters. To return several values, return a small struct — it names the
fields and composes cleanly.
structSplitHost {
std::string host;
int port;
};
SplitHost split_authority(std::string_view authority); // clear at call site
Declare a function constexpr when it can run at compile time (F.4) and
noexcept when it must not throw (F.6); prefer pure functions with no
observable side effects (F.8). Never return a reference or pointer to a
local (F.43), and do not return const by value — it silently blocks
moves (F.49).
Classes and Hierarchies (C.*)
Use struct when members vary independently and class when an invariant
ties them together (C.2). Expose the minimum (C.9). The special-member
decision has two clean answers and one trap.
Rule of Zero (C.20) — the default. If every member already manages
itself, declare no special members and let the compiler synthesize all of
them correctly:
structInvoice {
std::string customer;
std::vector<LineItem> lines;
std::chrono::system_clock::time_point issued;
// No destructor, no copy/move — the members handle it.
};
Rule of Five (C.21) — only when you own a raw resource. If you write
(or =delete) any one of destructor, copy, or move, account for all five.
Here a class owns a POSIX file descriptor:
For polymorphic bases, make the destructor either public-and-virtual or
protected-and-non-virtual (C.35), suppress public copy/move on the base
(C.67), and mark each overriding function with exactly one of virtual,
override, or final (C.128):
Make single-argument constructors explicit (C.46) so they do not become
accidental implicit conversions. Do not call virtual functions from a
constructor or destructor (C.82) — dynamic dispatch is not yet (or no
longer) wired to the derived type.
Resource Management (R.*)
RAII is the whole game. Ownership lives in unique_ptr or shared_ptr
(R.20); a raw T* is a non-owning observer and nothing more (R.3).
Prefer unique_ptr and reach for shared_ptr only when ownership is
genuinely shared (R.21).
Avoid explicit new / delete (R.11) and malloc / free (R.10) in
C++ code — a smart pointer or a scoped object expresses the intent and can
never leak on an early return or an exception. Prefer scoped stack objects
to heap allocation when the lifetime is local (R.5), and do not allocate
two resources in a single expression, which can leak if the second
allocation throws (R.13).
Expressions and Statements (ES.*)
Keep scopes small (ES.5) and always initialize (ES.20). Default to
brace initialization (ES.23), which also rejects narrowing conversions,
and declare objects const unless you intend to mutate them (ES.25).
constint max_attempts{3};
const std::string prefix{"ceo-"};
const std::vector<int> primes{2, 3, 5, 7, 11};
// A lambda initializes a complex const in one immutable step (ES.28).constauto settings = [&] {
Settings s;
s.timeout = std::chrono::seconds{30};
s.attempts = max_attempts;
return s;
}();
Recurring corrections in review:
Use nullptr, never 0 or NULL, for pointers (ES.47).
Replace C-style casts with a named cast — static_cast, and only when
unavoidable reinterpret_cast / const_cast (ES.48); never cast away
const (ES.50).
Name your constants; no magic numbers (ES.45).
Do not mix signed and unsigned arithmetic (ES.100); avoid narrowing or
lossy conversions (ES.46).
Do not shadow a name in a nested scope (ES.12).
Error Handling (E.*)
Decide the error strategy up front (E.1). Throw an exception to signal
that a function cannot complete its job (E.2), and lean on RAII so a throw
never leaks (E.6). Use purpose-built exception types (E.14), throw by
value, and catch by reference (E.15).
classConfigError : public std::runtime_error {
public:
using std::runtime_error::runtime_error;
};
classMissingKeyError : public ConfigError {
public:
explicitMissingKeyError(std::string key)
: ConfigError("missing key: " + key), key_(std::move(key)) {}
const std::string& key()constnoexcept{ return key_; }
private:
std::string key_;
};
Value require(const Config& cfg, const std::string& key){
if (!cfg.contains(key)) throwMissingKeyError(key); // E.2return cfg.at(key);
}
voidload(){
try {
require(current_config(), "endpoint");
} catch (const MissingKeyError& e) { // catch by referencereport(e.what(), e.key());
}
// Do not blanket-catch here — let the unexpected propagate (E.17).
}
Destructors, deallocation, and swap must never fail (E.16) — mark them
noexcept. Do not throw built-in types or string literals (E.14), do not
catch by value (slicing), and do not use exceptions for ordinary control
flow (E.3).
Constants and Immutability (Con.*)
Immutable by default, top to bottom: objects (Con.1), member functions
(Con.2), and pointer/reference parameters (Con.3). Use const for
values fixed after construction (Con.4) and constexpr for values a
compiler can fold (Con.5).
Think in tasks, not raw threads (CP.4), and minimize writable data shared
across them (CP.2, CP.3). Every lock is RAII and every lock guard is
named — an unnamed guard is a temporary that unlocks immediately (CP.20,
CP.44). Wait only on a predicate (CP.42).
classBoundedCounter {
public:
voidincrement(){
std::lock_guard<std::mutex> lock(m_); // named — held to scope end
++count_;
}
intdrain(){
std::unique_lock<std::mutex> lock(m_);
cv_.wait(lock, [this] { return count_ > 0; }); // predicate, CP.42return std::exchange(count_, 0);
}
private:
std::mutex m_; // mutex sits with the data it guards
std::condition_variable cv_;
int count_{0};
};
Take multiple mutexes with std::scoped_lock, which orders them to avoid
deadlock (CP.21):
Do not use volatile for synchronization — it is for memory-mapped
hardware, not threads (CP.8). Do not call unknown code (a user callback)
while holding a lock (CP.22). Avoid detached threads (CP.26) and
lock-free programming (CP.100) unless there is no alternative.
Templates and Generics (T.*)
Constrain every template parameter with a concept (T.10), preferring the
standard concepts (T.11). Concepts move a cryptic instantiation failure to
a clear, early diagnostic at the call site.
#include<concepts>template <std::integral T>
constexpr T gcd(T a, T b)noexcept{
while (b != T{0}) { a = std::exchange(b, a % b); }
return a;
}
// A domain concept states the requirement a type must satisfy.template <typename T>
concept Encodable = requires(const T& t) {
{ t.to_bytes() } -> std::convertible_to<std::vector<std::byte>>;
};
template <Encodable T>
voidpersist(const T& value, const Path& where);
Prefer using aliases over typedef (T.43), overload rather than
specialize a function template (T.144), and reserve template
metaprogramming for cases where constexpr genuinely cannot do the job
(T.120).
Standard Library (SL.*)
Prefer the standard library to hand-rolled equivalents (SL.1, SL.2).
Use std::vector by default and std::array for fixed size (SL.con.1,
SL.con.2); std::string owns characters and std::string_view observes
them (SL.str.1, SL.str.2). Write '\n' rather than std::endl, which
forces an unwanted flush on every line (SL.io.50).
Prefer enumerations to macros (Enum.1) and scoped enum class to plain
enum (Enum.3) — scoping prevents name leakage and accidental integer
conversions. Do not spell enumerators in ALL_CAPS (Enum.5).
enum classSeverity { trace, info, warning, error }; // scoped, not ALL_CAPS// Avoid: leaks names into the enclosing scope and collides with macros.enum { RED, GREEN, BLUE };
Headers and Naming (SF., NL.)
A header is self-contained — it includes everything it uses (SF.11) — and
is protected by an include guard or #pragma once (SF.8). Never put
using namespace at global scope in a header (SF.7). Pick one consistent
naming style (NL.8), reserve ALL_CAPS for macros only (NL.9), and do not
encode types into names (no Hungarian notation) (NL.5).
#ifndef CEO_NET_TCP_CONNECTION_H#define CEO_NET_TCP_CONNECTION_H#include<string>#include<string_view>namespace ceo::net {
classtcp_connection {
public:
explicittcp_connection(std::string host, int port);
voidsend(std::string_view payload);
boolis_open()const;
private:
std::string host_; // trailing underscore marks a data memberint port_;
};
} // namespace ceo::net#endif// CEO_NET_TCP_CONNECTION_H
Performance (Per.*)
Do not optimize without a reason (Per.1), do not optimize prematurely
(Per.2), and never claim a speedup without a measurement (Per.6). When
you do optimize, move work from run time to compile time (Per.11) and lay
data out for predictable, contiguous access (Per.19).
// Build a lookup table once, at compile time (Per.11).constexprauto squares = [] {
std::array<int, 256> t{};
for (int i = 0; i < 256; ++i) t[i] = i * i;
return t;
}();
std::vector<Point> hot_path; // contiguous — cache-friendly
std::vector<std::unique_ptr<Point>> pointer_chase; // avoid on a hot path
Review Checklist
Before you call C++ work done:
No raw new / delete; ownership is in a smart pointer or scoped
object (R.11).
Every object is initialized where it is declared (ES.20).
Objects and member functions are const / constexpr by default
(Con.1, Con.2, ES.25).
enum class, not plain enum (Enum.3); nullptr, not 0/NULL
(ES.47).
No narrowing conversions (ES.46) and no C-style casts (ES.48).
Single-argument constructors are explicit (C.46).
Rule of Zero, or a complete Rule of Five (C.20, C.21).
Polymorphic base destructors are public-virtual or
protected-non-virtual (C.35); overrides marked override (C.128).
Template parameters are concept-constrained (T.10).
No using namespace in a header at global scope (SF.7); headers are
guarded and self-contained (SF.8, SF.11).
Locks are RAII and named (CP.20, CP.44).
Exceptions are custom types, thrown by value, caught by reference
(E.14, E.15).
'\n' instead of std::endl (SL.io.50); no magic numbers (ES.45).
Changelog
1.0.0 — Initial release. Modern-C++ coding standard grounded in the
public C++ Core Guidelines, covering the six cross-cutting principles plus
interfaces, functions, classes, resource management, expressions, error
handling, immutability, concurrency, templates, standard library,
enumerations, headers/naming, performance, and a review checklist.
Skill-Import-Attestation: reviewed-by=AE9B236FDAF0462874060C6BCFCFACF00335DC74; sha256=5a8a0d0436db6fb5c1c8231c0000a441af1b6521dcfb0c132f02183cc2dc5818