C++ C++26 · GCC 15 · Clang 21 · the model, the costs and the traps · 619 entries

Written for someone who already knows C, knows what a vtable is, and wants the parts of C++ that are actually peculiar rather than the parts that are just C with different punctuation. The through-line is where the machine code comes from: what a value category costs, what the compiler synthesises for you and when it stops, what a shared_ptr allocates, what a virtual call and an exception really compile to, and which of the language’s conveniences are free and which are not. The language reference comes first — 175 entries covering every keyword, operator, fundamental type and attribute, because that is what you reach for with the editor still open; the keywords are set as keycaps, and hovering one raises a worked code example rather than a sentence. Below it are the 25 guide cards — the object model, value categories and moves, initialisation in its seven spellings, templates and the two-phase lookup that breaks them, overload resolution and ADL, exceptions and their guarantees, the container complexities and the iterator-invalidation table, ranges, lambdas, the memory model, and a catalogue of undefined behaviour that a sanitizer will find before your users do. The standard library index closes the sheet with 444 more entries. Every entry carries a dot saying which standard it arrived in — 171 of the 619 are newer than C++17.

Standard: C++17 or earlier C++20 C++23 C++26 deprecated / removed
Sources: ISO/IEC 14882 and its public working drafts (N4950 for C++23, the current C++26 draft), cppreference.com, the GCC 15 and Clang 21 manuals and their C++ status pages, and the libstdc++ and libc++ sources where the standard only states a bound. Compiled and run against Apple clang 21 and GCC 15. Hover any clipped row for the whole entry.

Start Here

what the language is, and how to read this page

C++ is C plus a compiler that writes code for you. Almost everything peculiar about the language follows from that one decision: constructors, copies, moves, destructors and template instantiations are generated on your behalf, and the rest of the language exists to say which ones, precisely enough that you can still predict the machine code. A C programmer reading C++ is usually not confused by the syntax — they are confused about what the compiler emitted that nobody wrote. That is the question this sheet keeps answering.

The consequence worth carrying: nothing here is free by default and nothing is expensive by default. A virtual call is one indirection; a std::string copy is an allocation; a range view is usually nothing at all. The cost is knowable in every case, and the guide cards below are largely a catalogue of which is which.

How this page is ordered. The language reference comes first — keywords, operators, fundamental types and attributes — because that is what you reach for with the editor open. Then the working guide, 25 cards on the model and the costs. Then the standard library index. Press / to jump to the filter box, which searches every row on the page at once.

How to read a row. Every entry carries a coloured dot for the standard it arrived in — C++17 or earlier C++20 C++23 C++26 deprecated or removed — and the dot is repeated in words in the row’s own tooltip, so a bullet is never unexplained. In the Keywords card the names are set as keycaps; hovering one raises a compiled code example rather than a sentence. Hovering any other row shows the whole entry, which matters because long lines are clipped.

Language Reference

The language itself — every keyword, operator, fundamental type and attribute, dotted by the standard it arrived in. Hover a keycap for a worked example; type in the filter box, or press /DotsC++17 or earlierC++20C++23C++26deprecated

Keywords — Declarations

27

Naming a type

autodeduce the type
decltypethe type of an expression
decltype(auto)deduce, preserving references
usingalias, or a using-declaration
typedefthe old alias syntax
typenamea template parameter, or a dependent type
templatedeclare a template, or disambiguate a dependent one
concepta named constraint
requiresa constraint clause or expression

Defining a type

classdefine a class; members private by default
structdefine a class; members public by default
unionoverlapping members
enumenumeration; enum class is the scoped form
friendgrant access
operatordeclare an overload
thispointer to the current object

Access and scope

publicvisible to everyone
privatevisible to the class and its friends
protectedvisible to the class and derived classes
namespacea named scope
exportpart of a module interface

Storage and linkage

staticinternal linkage, or per-class/per-function storage
externdeclare without defining
inlinemultiple definitions permitted
thread_localone instance per thread
registerreserved, and meaningless
auto (storage class)removed in C++11

Keywords — Statements

24

Selection

ifbranch
elsethe other branch
switchbranch on an integral or enum value
casea label inside a switch
defaultthe fallback label; also = default
if constexprcompile-time branch
if constevalam I in constant evaluation?

Iteration

forthe three-clause loop
whiletest, then body
dobody, then test
range-forfor (auto& x : range)

Jumping

breakleave the innermost loop or switch
continuenext iteration
returnleave the function
gotojump to a label in this function

Exceptions

tryguard a block
catchhandle one exception type
throwraise, or rethrow

Allocation, coroutines, and the odd one

newdynamic allocation
deleterelease, or suppress a member
co_awaitsuspend until ready
co_yieldproduce a value and suspend
co_returnfinish a coroutine
asminline assembly

Keywords — Specifiers

16

Qualifiers

constnot modifiable through this name
volatileaccesses may not be elided or reordered
mutablemodifiable in a const member

Compile time

constexprmay be evaluated at compile time
constevalmust be evaluated at compile time
constinitmust be constant-initialised
static_assertcompile-time assertion

Class members

explicitno implicit conversion
virtualdynamic dispatch, or a virtual base
overridemust override a base virtual
finalno further override or derivation

Contracts and layout

noexceptpromises not to throw; also an operator
alignasrequire an alignment
alignofquery an alignment
sizeofsize in bytes
typeidrun-time type information

Keywords — Casts & Literals

22

The casts

static_cast<T>related types, checked at compile time
dynamic_cast<T>checked downcast
const_cast<T>add or remove const/volatile
reinterpret_cast<T>reinterpret the bits
C-style (T)xtries the others in order
std::bit_cast<T>reinterpret bits, defined

Casts that do not look like casts

std::movecast to an rvalue reference
std::forwardpreserve the caller's value category

Literal keywords

nullptrthe null pointer constant
truethe boolean true
falsethe boolean false

Alternative tokens — reserved, and exactly equivalent

and&&
or||
not!
and_eq&=
or_eq|=
not_eq!=
bitand&
bitor|
xor^
xor_eq^=
compl~

Operators

29

Precedence, highest first

:: scope resolution
a++ a-- () [] . ->postfix
++a --a + - ! ~ * & (T) sizeof new deleteunary and casts
.* ->*pointer-to-member access
* / %multiplicative
+ -additive
<< >>shift
<=>three-way comparison
< <= > >=relational
== !=equality
& ^ |bitwise and, xor, or
&& ||logical, short-circuiting
?: = += -= *= …conditional and assignment
,comma

Overloadable, with their canonical shape

operator+ - * / %free function, by value
operator+= -= *=member, returns T&
operator++ --prefix returns T&, postfix takes int and returns T
operator=member only, returns T&
operator[]member only
operator()member only
operator->member; must return a pointer or something with ->
operator<< >>free function
operator boolmember, mark it explicit
operator<=>member; = default is usually right
operator new / deleteclass-level or global
operator""_xuser-defined literal

Cannot be overloaded

. .* :: ?:member access, scope, conditional
sizeof alignof typeidcompile-time queries
# ##preprocessor

Fundamental Types

29

Integer types — the guarantees, not the sizes

voidno value, or an incomplete type
charexactly 1 byte; signedness implementation-defined
signed char / unsigned charat least 8 bits
shortat least 16 bits
intat least 16 bits; 32 in practice everywhere
longat least 32 bits
long longat least 64 bits
booltrue or false
char8_tUTF-8 code unit
char16_t / char32_tUTF-16 / UTF-32 code units
wchar_timplementation-defined width

Fixed-width, from <cstdint>

int8_t … int64_texactly N bits, two’s complement
uint8_t … uint64_texactly N bits, unsigned
int_fast8_t / int_least8_tfastest / smallest with at least N bits
intptr_t / uintptr_tlarge enough for a pointer
intmax_t / uintmax_tthe widest integer
size_t / ptrdiff_tsizeof result / pointer difference
std::bytea byte that is not a number
std::nullptr_tthe type of nullptr

Floating point

float / double / long doubleIEEE-754 binary32 / binary64 / platform
std::numeric_limits<T>the properties of a numeric type
std::float32_t / float64_tfixed-width floats
NaN comparisonsalways false, including NaN == NaN

Integer conversions & UB

signed overflowundefined behaviour
unsigned overflowdefined: wraps modulo 2^N
signed-to-unsigned conversiondefined: modulo 2^N
std::cmp_less / cmp_equalcompare mixed signedness correctly
signed integers are two’s complementguaranteed since C++20
narrowing in bracesa compile error

Declarations & Attributes

28

Structured bindings and friends

auto [a, b] = pair;structured binding
auto& [a, b] = obj;bind by reference
for (auto [k, v] : map)the idiom this was built for
auto [a, b] in a lambda capturestill not allowed directly
auto f() -> Ttrailing return type
auto f()deduced return type

Standard attributes

[[nodiscard]]warn if the result is ignored
[[maybe_unused]]suppress the unused warning
[[fallthrough]]this switch fall-through is deliberate
[[deprecated]]warn on use
[[noreturn]]never returns
[[likely]] / [[unlikely]]branch hint
[[no_unique_address]]may share storage with another member
[[assume(expr)]]the optimiser may assume this
__attribute__((…))GCC/Clang extension

Namespaces

namespace N { … }named scope
namespace A::B::Cnested definition
namespace { … }unnamed — internal linkage
inline namespace V1 { … }members appear in the parent
namespace fs = std::filesystem;namespace alias
using namespace N;bring it all in
using N::f;using-declaration

Linkage & the preprocessor

extern "C" { … }C linkage — no mangling
#pragma onceinclude guard
__has_include(<x>)is this header available?
__cplusplusthe standard version
<version>all library feature-test macros
#embed "file"embed a file as data

The Working Guide

C++ as the compiler and the standard actually define it — the model, the costs and the traps

What C++ Actually Is

the bargain, and what it costs

One sentence explains most of the language: you do not pay for what you do not use, and what you do use you could not hand-code better. Every design decision that looks strange follows from defending that. Templates are compiled per instantiation rather than boxed, so generic code is as fast as hand-written code and your build is slow. Destructors run deterministically at scope exit, so there is no collector and no pause, and every resource bug becomes a lifetime bug instead. Exceptions cost nothing on the path that does not throw, so the throw path costs a table walk.

The four things that are genuinely not C

Value semanticsa class object is a value like an int: copied, moved, destroyed on schedule — not a handle to a heap thingRAIIa destructor is the only cleanup mechanism, and it always runs; this replaces goto fail, finally, and deferTemplatesa compile-time function from types to code, checked at instantiation, not a macro and not a genericOverload resolutionthe compiler picks a function from a candidate set by ranking conversions — the single biggest source of surprise

What each abstraction actually costs

ConstructRuntime costHidden cost
std::vector<T>identical to a hand-rolled malloc'd arraygrowth reallocates and moves; capacity is not size
std::unique_ptr<T>zero — same code as a raw pointer plus a deletenone, if the deleter is stateless
std::shared_ptr<T>two pointers wide, atomic inc/dec on every copya second allocation unless you use make_shared
std::function<R(A)>an indirect call; heap allocation if the target is bigtype erasure defeats inlining — prefer a template parameter
A lambdazero — it is a struct with an operator()none; it is the std::function around it that costs
virtual callone load of the vptr, one indexed load, one indirect callblocks inlining and devirtualisation; +8 bytes per object
thrownothing until it fires; then unwinding tablesbinary size; -fno-exceptions changes the library ABI
std::stringSSO: short strings never touch the heaplibstdc++ 32 bytes / 15 chars, libc++ 24 bytes / 22 chars
The rule that pays for itself. If you can see the type at compile time, the optimiser can inline through it and the abstraction is free. The moment you erase it — virtual, std::function, a void* — you have bought an indirect call. That is the whole performance model.

Dialects you will meet

-std=c++17the pragmatic floor; every toolchain and every embedded vendor has it-std=c++20concepts, ranges, spaceship, coroutines, modules-on-paper-std=c++23deducing this, std::expected, std::print, mdspan-std=c++26reflection, contracts, std::execution; partial in GCC 15 / Clang 21-fno-exceptions -fno-rttithe embedded dialect; not standard C++ and not ABI-compatible with the rest

Compiling and Linking

what the driver does that C does not

The four phases are the same as C. What is different is that the compiler now emits far more than you wrote, and the linker has to sort out the duplicates.

c++ -std=c++23 -O2 -Wall -Wextra a.cpp -o progthe normal invocationc++ -c a.cpp→ a.oc++ -E a.cpp | wc -la hello-world with <iostream> is ~40,000 linesc++ -fsyntax-only a.cppparse and check; the fastest way to find a template errorc++ -ftime-reportwhere the build went — usually template instantiationc++ -Xclang -ast-dump -fsyntax-only a.cppthe AST, when overload resolution surprises you

Name mangling — why nm output is unreadable

C++ encodes the whole signature into the symbol so overloads can coexist in one object file. Both GCC and Clang use the Itanium C++ ABI on every platform except Windows/MSVC.

_Z3fooifoo(int) — _Z, length-prefixed name, parameter codes_Z3fooPKcfoo(const char*) — P pointer, K const, c charnm -C progdemangle while listing (or c++filt)c++filt _Z3fooidemangle one symbol by hand
The ABI break you will hit. libstdc++ has had two std::string ABIs since GCC 5. -D_GLIBCXX_USE_CXX11_ABI=0 selects the old one. Mixing them gives you link errors naming __cxx11::basic_string — the symptom is an undefined reference to a function you can plainly see is defined.

The One Definition Rule, and the four ways out of it

A non-inline function or variable may be defined once in the whole program. Headers are textually included into every TU, so anything a header defines needs an exemption:

ExemptionApplies toMechanism
inlinefunctions, and since C++17 variablesemitted weakly in each TU; linker keeps one
member functions defined in-classimplicitly inlinesame
templatesimplicitly, per instantiationCOMDAT sections, deduplicated at link
constexprimplicitly inline for functionssame
unnamed namespaceanythinginternal linkage — a separate entity per TU
ODR violations are silently ill-formed, no diagnostic required. Two TUs that define the same class differently — different member order, a different -DNDEBUG, a different -std= — link fine and then behave as if memory were corrupted, because the linker picked one definition arbitrarily. Build every TU with the same flags. gold --detect-odr-violations and ASan's ODR checker catch the easy cases.

Link order still matters, and now there is more of it

c++ a.o -lfoo -lbarstatic libraries are scanned left to right, once — dependents first-Wl,--start-group … -Wl,--end-groupthe sledgehammer for circular static libsld: symbol(s) not found for architecture arm64usually a missing definition of a virtual, or a mangling mismatchundefined reference to `vtable for X'you declared a virtual and never defined it — see the key-function rule below

The key function rule. Under the Itanium ABI the vtable is emitted in the TU that defines the class's first non-inline, non-pure virtual member. Declare all your virtuals inline and there is no key function, so the vtable is emitted everywhere (bigger binary). Declare one and never define it, and it is emitted nowhere — giving exactly the "undefined reference to vtable" that confuses everyone the first time.

The Object Model

layout, vptr, and what sizeof tells you

A C++ object is a C struct plus, if it needs them, one or more vptrs. Nothing else is added. Everything about polymorphism follows from where those pointers sit.

Layout rules

standard-layoutno virtuals, no mixed access control, one class in the hierarchy has data → C-compatible layout, offsetof is legaltrivially copyableno user copy/move/destructor → memcpy is a legal copyPODboth of the above; the term is deprecated in favour of the two halvestrivialtrivially copyable and trivially default-constructible
Classsizeof on LP64Why
struct E {};1every object needs a distinct address
struct A { char c; int i; };83 bytes of padding before i
struct B { int i; char c; };83 bytes of tail padding — arrays must stay aligned
struct V { virtual ~V(); int i; };16vptr first, then i, then padding
struct D : E { int i; };4empty base optimisation — E takes no space
struct M { [[no_unique_address]] E e; int i; };4C++20: EBO for members too
Reorder members largest-first and the padding usually disappears. clang -Wpadded tells you where it went; pahole on the object file draws the picture.

Single inheritance: a prefix, and one vptr

struct Base { virtual void f(); int a; }; struct Derived : Base { void f() override; int b; }; Derived object vtable for Derived +0 vptr ------------------> [ offset-to-top: 0 ] +8 a (from Base) [ typeinfo* -> Derived ] +12 b [ &Derived::f ]

Because Base's members sit at the front, a Derived* converts to a Base* with no address change. A virtual call is load vptr; load vtable[n]; call — three instructions and one unpredictable branch.

Multiple inheritance: more than one vptr, and pointer adjustment

With struct D : A, B the B subobject cannot also be at offset 0. Converting D*B* adds a non-zero offset, and calling a B virtual through it must subtract that offset back before this is right. The compiler emits a thunk to do the subtraction.

Consequences you will actually meet. Casting a multiply-inherited pointer through void* and back is undefined — the offset is lost. Comparing a D* with a B* that point at the same object compares equal only because the compiler inserts the adjustment; do it through void* and they differ. And delete through a base pointer requires a virtual destructor, or the wrong offset is passed to operator delete.

Virtual inheritance: the diamond, and the vtt

struct B : virtual A makes the A subobject shared, so its position is not known until the most-derived class is known. The compiler adds a virtual base offset in the vtable and, during construction, a VTT (virtual table table) to walk the partially-built object. The most-derived class — not the intermediate ones — constructs the virtual base. Cost: an extra indirection on every access to a virtual base member. Use it for interface mixins; never for data.

RTTI, and what it is really for

typeid(x)const std::type_info&; polymorphic if x has virtuals, else staticdynamic_cast<D*>(bp)→ nullptr on failure; walks the hierarchy at run time — not cheapdynamic_cast<D&>(br)throws std::bad_cast on failuredynamic_cast<void*>(p)→ address of the most-derived object; the one legitimate identity teststatic_cast downno check — UB if the object is not really a D

A dynamic_cast in a hot loop is usually a design smell, but it is not forbidden; measure before you contort the design to avoid it. -fno-rtti removes typeid and dynamic_cast entirely and is not linkable against a library built with them.

Value Categories

lvalue, xvalue, prvalue — and why

Every expression has a type and a value category. Since C++11 there are three, defined by two independent questions: does it have identity (can you take its address), and can it be moved from.

has identityno identity
cannot movelvalue — a named variable, *p, a function call returning T&
can movexvaluestd::move(x), a call returning T&&, a[i] on an rvalue arrayprvalue — a literal, a+b, a call returning T by value

"glvalue" = lvalue or xvalue (has identity). "rvalue" = xvalue or prvalue (movable). The names are awful; the distinction is not. It is what decides which overload you get and whether a temporary's storage is elided.

Which reference binds to what

T&lvalues onlyconst T&everything — and extends a temporary's lifetime to the reference's scopeT&&rvalues only (xvalue or prvalue)const T&&legal, nearly useless; exists so std::move on a const object still compiles (and copies)auto&&a forwarding reference in a deduced context — binds to anything, see the templates card
A named rvalue reference is an lvalue. Inside void f(T&& x), the parameter x is an lvalue — it has a name and an address. Passing it on without std::move(x) copies. This is the single most common move bug.

Guaranteed copy elision (C++17) — the rule that changed the language

Since C++17, a prvalue is not a temporary object that gets copied; it is an initialiser for whatever object it ends up in. There is no copy or move to elide because none was ever notionally there.

struct S { S(); S(const S&) = delete; S(S&&) = delete; }; S make() { return S{}; } // legal in C++17; ill-formed in C++14 S s = make(); // exactly one S is ever constructed
CaseC++14C++17
return S{}; (prvalue)copy elision permitted; copy ctor must existguaranteed; no copy/move ctor needed
return local; (NRVO)permitted, not guaranteedstill only permitted — falls back to a move
return std::move(local);defeats NRVOdefeats NRVO — do not write it
Never return std::move(x) for a local. It turns a return that the compiler could elide entirely into a guaranteed move, and -Wpessimizing-move will tell you so. The exception is returning a member or a by-value parameter, where NRVO does not apply anyway.

decltype reads value category back out

decltype(x)the declared type of the entity x — no references addeddecltype((x))note the parens: an expressionT& for an lvaluedecltype(auto)deduce with decltype rules — preserves references, unlike plain autodecltype(f())T, T& or T&& exactly as f declares it

Move Semantics

what a move actually is

A move is not a language operation. It is an ordinary overload — a constructor or assignment operator taking T&& — that is allowed to gut its argument because the caller has promised not to use the value again. std::move does not move anything; it is a cast.

template <class T> constexpr std::remove_reference_t<T>&& move(T&& t) noexcept { return static_cast<std::remove_reference_t<T>&&>(t); } // that is the entire implementation

The contract on a moved-from object

The standard requires only that a moved-from standard-library object is in a valid but unspecified state: you may destroy it, and you may assign to it. You may not assume it is empty. std::string usually is; std::vector usually is; a moved-from unique_ptr is guaranteed null, because that is specified separately.

class Buf { char* p_ = nullptr; size_t n_ = 0; public: Buf(Buf&& o) noexcept : p_(o.p_), n_(o.n_) { o.p_ = nullptr; o.n_ = 0; } // steal, then NULL the source Buf& operator=(Buf&& o) noexcept { if (this != &o) { delete[] p_; p_ = o.p_; n_ = o.n_; o.p_ = nullptr; o.n_ = 0; } return *this; } ~Buf() { delete[] p_; } };
noexcept on a move constructor is not decoration. std::vector reallocation uses move_if_noexcept: if your move constructor is not noexcept, the vector copies every element instead, to keep the strong exception guarantee. A missing noexcept silently costs you the entire benefit of move semantics on your hottest path.

Forwarding references and std::forward

T&& is an rvalue reference except when T is a template parameter being deduced in that position — then it is a forwarding reference and reference collapsing applies:

T& & → T&lvalue winsT& && → T&lvalue winsT&& & → T&lvalue winsT&& && → T&&only rvalue+rvalue stays an rvalue
template <class T> void wrapper(T&& arg) { target(std::forward<T>(arg)); } // preserves the caller's category // lvalue in: T deduces to U&, arg is U& → forward gives U& // rvalue in: T deduces to U, arg is U&& → forward gives U&&
Rule of thumb. std::move on an rvalue reference, std::forward on a forwarding reference, and each exactly once, on the last use. Moving twice is not a compile error and usually leaves you with an empty string in production.

Where a forwarding-reference constructor eats your copy constructor

struct Greedy { template <class T> Greedy(T&& x); // deduces T = Greedy& for a non-const lvalue Greedy(const Greedy&); // never chosen for a NON-const Greedy lvalue! }; Greedy a; Greedy b(a); // calls the template, not the copy constructor

Fix with a constraint: requires (!std::same_as<std::remove_cvref_t<T>, Greedy>), or pre-C++20 an enable_if. This is the classic argument for constraining every forwarding-reference constructor you write.

Rules of Zero, Three and Five

what the compiler writes for you

The compiler will synthesise six special members. Which ones it synthesises depends on what you declared, and the interactions are the least memorable part of the language — hence the table.

You declaredefault ctorcopy ctorcopy=move ctormove=dtor
nothingyesyesyesyesyesyes
any constructornoyesyesyesyesyes
a destructoryesyes(d)yes(d)nono
copy ctor or copy=yesthe other is deprecated-but-generatednonoyes
move ctor or move=yesdeleteddeletedthe other is not generatedyes

(d) generated, but its generation is deprecated; -Wdeprecated-copy warns. The whole table reduces to one line: declaring any of the five suppresses some of the others.

The silent pessimisation. Add a destructor — even an empty one, even just to put a breakpoint in — and the move constructor stops being generated. Every move of that type becomes a copy. Nothing warns. This is why the Rule of Zero is not stylistic advice.

Rule of Zero

Write none of the five. Hold resources in members that manage themselves — vector, string, unique_ptr — and the compiler-generated members are correct, optimal and free. Every class you write should aim at this; a class that manages a resource is a separate, tiny class.

Rule of Five

If you must write one, write all five, or = delete them deliberately.

class Res { Handle h_; public: explicit Res(const char* n) : h_(open(n)) {} ~Res() { if (h_) close(h_); } Res(const Res&) = delete; // not copyable Res& operator=(const Res&) = delete; Res(Res&& o) noexcept : h_(std::exchange(o.h_, {})) {} // exchange says it in one line Res& operator=(Res&& o) noexcept { if (this != &o) { if (h_) close(h_); h_ = std::exchange(o.h_, {}); } return *this; } };

Copy-and-swap, and why it is out of fashion

T& operator=(T other) { swap(*this, other); return *this; } // by VALUE

One operator handles both copy- and move-assignment, is self-assignment-safe for free, and is strongly exception safe. The cost is one extra move in the copy case, and it prevents assignment from reusing existing capacity — which for a vector-like type is a real loss. Correct by construction, slower than a hand-written pair. Use it when the type is not on a hot path.

= default is not the same as writing it out

T() = default;trivial if it can be — the object may be left uninitialisedT() {}user-provided; never trivial; value-init does NOT zero the membersT() = default; T t{};value-initialisation of a trivial default ctor DOES zero~T() = default;still suppresses the move members — the trap above applies to defaulted destructors too

Initialisation

seven spellings, and the vexing parse

C++ has more initialisation syntaxes than it has meanings for them, and the mismatches are where the surprises live.

SpellingNameWhat it does
T t;default-initcalls the default ctor; for a trivial type, leaves it uninitialised
T t{};value-initzero-initialises then default-constructs — the safe default
T t(a, b);direct-initordinary overload resolution over all constructors
T t{a, b};list-initinitializer_list ctors are preferred; narrowing is an error
T t = a;copy-initno explicit ctors considered
T t = {a, b};copy-list-initas list-init, but no explicit ctors
T t = T(a);since C++17, identical to T t(a) — no temporary
Braces prefer initializer_list so hard it hurts.
std::vector<int> v(3, 0); → three zeros.
std::vector<int> v{3, 0};two elements, 3 and 0.
If a class has an initializer_list constructor, braces will find it even when another constructor is a better match — and will only fall through if no conversion to the list's element type exists at all.

The most vexing parse

Widget w(); // declares a FUNCTION taking nothing, returning Widget Widget w(Gadget()); // a function taking a function pointer! not a Widget Widget w{}; // an object. Braces cannot be parsed as a declaration. Widget w{Gadget{}}; // an object.

Anything that can be parsed as a declaration is. Braces are the cure, which is the strongest argument for the "almost always braces" style — with the initializer_list exception above as the standing counter-example.

Aggregate initialisation

An aggregate has no user-declared constructors, no private or protected non-static data, no virtuals. It is initialised member-by-member from braces, and since C++17 base classes count as leading members.

struct P { int x; int y = 5; }; P a{1, 2}; // x=1, y=2 P b{1}; // x=1, y=5 (default member initialiser) P c{}; // x=0, y=5 (value-init zeroes x) P d{.x = 1, .y = 2}; // C++20 designated initialisers -- IN DECLARATION ORDER, no gaps skipped over
C++20 designated initialisers are not C99's. They must appear in declaration order, cannot be nested arbitrarily, and cannot be mixed with positional initialisers. Out-of-order compiles in C and is an error in C++.

Member initialisation order

class X { int a_, b_; public: X(int n) : b_(n), a_(b_ * 2) {} // WRONG: a_ is initialised FIRST -- b_ is still garbage }; // -Wreorder catches exactly this

Members are initialised in declaration order, never in the order written in the member-init list. Bases first, then members, then the constructor body. Destruction is exactly the reverse.

Static initialisation order

The static initialisation order fiasco. The order in which namespace-scope objects in different translation units are constructed is unspecified. A global that uses another global from another TU is a bug that depends on link order. The fix is the Construct On First Use idiom — a function-local static, whose initialisation is lazy and, since C++11, thread-safe:
Registry& registry() { static Registry r; return r; }

const, constexpr, consteval

four keywords, four different questions
KeywordQuestion it answersSince
constmay I modify it through this name?C++98
constexprmay this be evaluated at compile time?C++11
constevalmust this be evaluated at compile time?C++20
constinitmust this static be initialised at compile time? (it stays mutable)C++20

const is about the access path, not the object

const int* ppointer to const int — cannot write *pint* const pconst pointer — cannot rebind pconst int* const pbothread right-to-leftint const* p is the same as the first, and reads more consistently

Physical vs logical const. A const member function promises not to change the object's observable state; mutable members (a cache, a mutex) are exempt. Since C++11, const also means thread-safe to call concurrently — that is the contract the standard library relies on, so a mutable cache in a const function needs its own synchronisation.

const_cast away and then write, and it is undefined if the object was declared const. It is legal only for an object that was not originally const — which is to say, for adapting to an old C API that forgot its consts.

What constexpr actually promises

constexpr int f(int n)may run at compile time; runs at run time if the arguments are not constantconstexpr int v = f(3);forces compile-time evaluation — a compile error if it cannotif constexpr (cond)C++17: the untaken branch is not instantiated — this is what replaced tag dispatchif constevalC++23: am I currently in a constant evaluation?std::is_constant_evaluated()C++20: the function-call form of the same question
template <class T> auto describe(T v) { if constexpr (std::is_pointer_v<T>) return *v; // only compiled when T IS a pointer else return v; }

What a constexpr function may do, by standard

SinceNewly allowed at compile time
C++11a single return statement
C++14loops, local variables, multiple statements, mutation
C++17if constexpr; constexpr lambdas
C++20new/delete (must be freed in the same evaluation), try, virtual calls, constexpr unions, std::vector and std::string
C++23non-literal variables, goto, static in constexpr functions, constexpr cmath
C++26constexpr exceptions; constexpr placement new; static reflection
constexpr on a variable is a much stronger claim than on a function. On a function it is permission; on a variable it is a requirement, checked immediately. If you want the requirement on a call, assign it to a constexpr variable or use consteval.

Compile-time strings, the C++20 way

template <std::size_t N> struct fixed_string { char data[N]{}; consteval fixed_string(const char (&s)[N]) { std::copy_n(s, N, data); } }; template <fixed_string S> void log(); // a string as a template argument -- C++20 NTTP

Templates

the instantiation model

A template is not code. It is a recipe the compiler runs once per distinct set of arguments, emitting a separate function or class each time. That is why C++ generics are as fast as hand-written code, why the error messages are enormous, and why templates must live in headers.

The two-phase lookup that catches everyone

The compiler parses a template twice. At definition it checks everything that does not depend on the template parameters; at instantiation it checks the rest. Names are looked up in the corresponding phase, and a dependent name is not looked up until phase two.

template <class T> struct Base { void helper(); }; template <class T> struct Derived : Base<T> { void f() { helper(); } // ERROR: helper is a non-dependent name, looked up in phase 1, // and Base<T> is not examined -- it depends on T void g() { this->helper(); } // OK: this-> makes it dependent void h() { Base<T>::helper(); } // OK: also dependent };
MSVC historically did not implement two-phase lookup, so a great deal of code compiles there and fails on GCC and Clang with "there are no arguments to 'helper' that depend on a template parameter". The fix is always this->.

typename and template as disambiguators

template <class T> void f() { typename T::value_type x; // without typename, T::value_type is assumed to be a VALUE T::template rebind<int> r; // without template, < is parsed as less-than }

C++20 made typename implicit in the places where only a type could appear (return types, member declarations, trailing return types, parameter declarations), which removes most of the noise but not all of it.

Deduction: what the compiler will and will not do

SituationResult
template<class T> void f(T) with const int&T = int — top-level const and references are stripped
template<class T> void f(T&) with const intT = const int — const is preserved
template<class T> void f(T&&) with an lvalueT = U& — forwarding reference
array argument to f(T)decays to a pointer; to f(T&) it does not, and T = U[N]
two parameters, two conflicting deductionshard error — no conversions are applied to make them agree
a non-deduced context (typename T::type)not deduced; must be supplied or deduced elsewhere

CTAD — class template argument deduction (C++17)

std::pair p{1, 2.0};pair<int,double>; no make_pair neededstd::vector v{1,2,3};vector<int>std::lock_guard g{m};lock_guard<mutex>template<class T> Box(T) -> Box<T>;an explicit deduction guide, when the constructor is not enough

Specialisation and the partial-ordering rule

template <class T> struct S { static constexpr int v = 0; }; // primary template <class T> struct S<T*> { static constexpr int v = 1; }; // PARTIAL -- classes only template <> struct S<int> { static constexpr int v = 2; }; // full
Function templates cannot be partially specialised. Attempting it silently gives you an overload instead, with different rules: a non-template function beats a template on an equal match, and full specialisations do not participate in overload resolution at all — they are chosen only after the primary template has already won. Prefer overloading, if constexpr, or constraints; reach for template<> almost never.

SFINAE, and what replaced it

"Substitution failure is not an error": if substituting deduced arguments produces an invalid signature, that candidate is silently removed rather than being a hard error. An error in the body is still an error.

// C++11/14 template <class T, class = std::enable_if_t<std::is_integral_v<T>>> void f(T); // C++17 template <class T> void f(T) { static_assert(std::is_integral_v<T>); } // clearer error, but not SFINAE-friendly // C++20 -- say what you mean template <std::integral T> void f(T);

Reading a template error

read bottom-upthe last "required from here" is your line; everything above is the library-fmax-errors=1 / -ferror-limit=1stop after the first; the rest are consequences-fconcepts-diagnostics-depth=2GCC: why a concept was not satisfied, not just that it was notstatic_assert earlya checked precondition beats a 300-line instantiation trace

Concepts and Constraints

C++20: saying what a template needs

Concepts move a template's requirements from the error message into the declaration. The practical win is not elegance — it is that a failed call now names the requirement it failed, and that constrained overloads can be ordered by how specific they are.

template <class T> concept Hashable = requires(T a) { { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>; }; template <Hashable T> void store(T); // constrained template parameter void store(Hashable auto); // abbreviated -- same thing template <class T> requires Hashable<T> void store(T); // requires-clause template <class T> void store(T) requires Hashable<T>; // trailing requires-clause

The four kinds of requirement in a requires expression

typename T::value_type;type requirement — the name must exist and be a typea + b;simple requirement — the expression must merely compile{ a.size() } -> std::convertible_to<size_t>;compound — compiles, and the result satisfies the concept{ a.f() } noexcept;compound, and must be non-throwingrequires Sortable<T>;nested requirement — another constraint must hold
requires requires is not a typo. The first introduces a requires-clause, the second a requires-expression: template<class T> requires requires(T a){ a.f(); } void g(T);. It compiles, but naming the concept is nearly always clearer.

Subsumption — the part that earns its keep

The compiler decomposes constraints into atomic pieces and prefers the overload whose constraints subsume the other's. This gives ordered overloads without tag dispatch:

template <std::input_iterator I> void adv(I& i, int n); // generic template <std::random_access_iterator I> void adv(I& i, int n); // wins for vector::iterator // random_access_iterator is defined in terms of input_iterator, so it subsumes it.
Subsumption works on the syntactic form, not on meaning. Two concepts that are logically identical but written differently do not subsume each other — and a constraint written with std::is_integral_v<T> is one opaque atom, while std::integral<T> decomposes. That is the practical reason to build constraints out of named concepts rather than out of type traits.

The standard concepts worth knowing by heart

HeaderConcepts
<concepts>same_as, derived_from, convertible_to, integral, floating_point, assignable_from, swappable, destructible, constructible_from, copyable, movable, equality_comparable, totally_ordered, invocable, predicate
<iterator>input_iterator, forward_iterator, bidirectional_iterator, random_access_iterator, contiguous_iterator, sentinel_for, sized_sentinel_for
<ranges>range, view, borrowed_range, sized_range, viewable_range, common_range

Overload Resolution and ADL

how the compiler actually chooses

Three steps, in this order. Most surprises come from the fact that step 1 finishes before step 3 begins: a worse-matching function in a nearer scope beats a better-matching one further out, because the further one was never a candidate.

1. Name lookupbuild the candidate set: ordinary lookup + argument-dependent lookup2. Viabilitydiscard candidates whose parameter count is wrong or whose arguments will not convert3. Rankingchoose the best viable candidate; ambiguity is an error

Conversion ranks, best to worst

RankIncludes
Exact matchidentity, lvalue-to-rvalue, array/function-to-pointer, qualification (T*const T*)
Promotionbool/char/shortint, floatdouble
Conversionany other arithmetic conversion, derived→base pointer, anything→bool, →void*
User-definedone converting constructor or one conversion operator — never two in a row
Ellipsis... — the last resort

A non-template function beats a template on an equal-ranked match. Between two templates, the more specialised wins (partial ordering); between two constrained templates, the more constrained wins (subsumption).

Argument-dependent lookup

For an unqualified call, the compiler also searches the namespaces of the argument types. This is why std::cout << x finds std::operator<< without a using, and it is not optional.

namespace N { struct S{}; void f(S); } N::S s; f(s); // found by ADL -- no qualification, no using-declaration // the two-step swap idiom: give the user's overload a chance, fall back to std:: using std::swap; swap(a, b);
ADL and forwarding references make a greedy pair. A template template<class T> void f(T&&) in an associated namespace is an exact match for everything, so it wins over the overload you meant. This is exactly the Greedy constructor problem from the moves card, at namespace scope. Constrain it.

The hidden-friend idiom

struct Money { friend Money operator+(Money a, Money b) { … } // defined INSIDE the class friend bool operator==(Money, Money) = default; // C++20 };

A friend defined in-class is only findable by ADL — it is not a member of the enclosing namespace at all. It therefore never pollutes overload sets it has no business being in, never needs to be a template, and cuts compile time. This is the modern default for operators.

Name hiding in derived classes

struct B { void f(int); void f(double); }; struct D : B { void f(const char*); }; // hides BOTH of B::f D d; d.f(1); // ERROR -- int does not convert to const char* struct E : B { using B::f; void f(const char*); }; // fixes it

Declaring any f in the derived class hides every f in the base — overload resolution never sees them, because lookup stopped at the first scope that had the name. The using-declaration pulls them back in.

-Woverloaded-virtual catches the variant of this that costs you a virtual dispatch: a derived function that hides a base virtual instead of overriding it because the signature drifted. Marking every override override catches it at the declaration instead.

Classes and Inheritance

the parts that are not obvious

Access, and what it does not protect

publicanyoneprotectedderived classes — but only through a D, not through a Bprivatethe class and its friends; the default for classstructidentical to class except the default is public, for members and for base classes

Access control is not a security boundary and does not affect layout. private members are still there, still in sizeof, and still visible to a cast — the class definition is in the header either way.

Virtual functions

virtual void f();dispatched on the dynamic typevoid f() override;always write this — a compile error if it does not actually overridevoid f() final;no further override; enables devirtualisationstruct D final : Bno further derivationvirtual void f() = 0;pure virtual — may still have a definition, which a derived class can call explicitly
Never call a virtual from a constructor or destructor. During B's constructor the object is a B — the vptr has not been updated to D yet — so the call dispatches to B::f, not the override. A pure virtual gets you the runtime's pure virtual method called abort. The language is being consistent, not perverse: D's members are not constructed yet, so D::f could not safely run.

Default arguments are static, virtual functions are not

struct B { virtual void f(int x = 1) { … } }; struct D : B { void f(int x = 2) override { … } }; B* p = new D; p->f(); // calls D::f -- with x == 1. The default came from the STATIC type.

Never give a virtual function a default argument. If you need one, make the public non-virtual function carry the default and have it call a private virtual — the Non-Virtual Interface idiom, which also gives you a single place for pre- and post-conditions.

Slicing

void take(Base b); // BY VALUE Derived d; take(d); // the Derived part is sliced off; b is a Base, and behaves like one

Pass polymorphic types by reference or pointer, never by value. Making the base class abstract, or its copy constructor protected, makes the mistake a compile error.

explicit, and the C++20 form

explicit Box(int);no implicit Box b = 42;, no silent conversion in a callexplicit operator bool() const;usable in if/while — a "contextual" conversion — but not in arithmeticexplicit(cond) Box(T);C++20: conditionally explicit, which is how std::pair is specified

Deducing this (C++23) — one function instead of four

struct S { template <class Self> auto&& value(this Self&& self) { return std::forward<Self>(self).v_; } // replaces the const/non-const/&/&& quadruplication -- and gives a lambda a way to recurse T v_; };

CRTP: static polymorphism with no vptr

template <class D> struct Cmp { friend bool operator<(const D& a, const D& b) { return a.key() < b.key(); } }; struct Item : Cmp<Item> { int key() const; };

The base knows its derived type at compile time, so every call inlines. C++23's "deducing this" makes CRTP unnecessary for the common cases; it is still the tool for mixin interfaces.

Operator Overloading

and the spaceship

What may be overloaded, and how

member= [] () -> and all conversions must be membersfree functionpreferred for symmetric binary operators, so the left operand converts toohidden friendthe modern default — free, ADL-only, no namespace pollutioncannot overload. .* :: ?: sizeof alignof typeid and the preprocessor's #

The canonical forms

T& operator++() { …; return *this; } // prefix: return a reference T operator++(int) { T t=*this; ++*this; return t; } // postfix: unused int, return the OLD value T& operator+=(const T& o) { …; return *this; } friend T operator+(T a, const T& b) { a += b; return a; } // by value + reuse += std::ostream& operator<<(std::ostream& os, const T& t); // must be free -- os is the left operand

Three-way comparison (C++20)

struct P { int x, y; auto operator<=>(const P&) const = default; // gives < <= > >= for free, memberwise, in order bool operator==(const P&) const = default; // == is NOT generated by <=> -- declare it separately };
Return typeMeaningExample
std::strong_orderingequal means substitutableint, std::string
std::weak_orderingequivalent, but distinguishablecase-insensitive strings
std::partial_orderingsome pairs are unordereddouble — because of NaN
The rewriting rules do the work. a < b becomes (a <=> b) < 0; a != b becomes !(a == b); and if the operands are the wrong way round the compiler will reverse them rather than fail. Six operators from two declarations, and a heterogeneous comparison now works in both directions from one definition.
= default on <=> compares members in declaration order. That is almost never the ordering you want for a type with a natural key. Write it out when the order matters: return std::tie(a_,b_) <=> std::tie(o.a_,o.b_);

Conversion operators, and why to avoid them

struct Handle { operator int() const; // BAD: now Handle participates in every arithmetic overload set explicit operator bool() const; // GOOD: contextual conversion only int get() const; // BEST: say it };

User-defined literals

constexpr Len operator""_m(long double v)1.5_m — the leading underscore is required for user codeoperator""s / ""sv / ""hthe standard's own, in std::literalstemplate<char…C> auto operator""_b()the literal's characters as template arguments — compile-time parsing

Exceptions

what they cost and what they guarantee

The implementation is table-driven and zero-cost on the non-throwing path: no flag is set on entry, no check is made on return. What you pay for is binary size (the unwind tables) and a slow throw — typically microseconds, because the runtime has to walk the stack consulting those tables.

throw X{};copy-initialises the exception object into runtime-managed storagecatch (const X& e)always by const reference — by value slices, by pointer leakscatch (...)everything; usually paired with a rethrowthrow;rethrow the current exception, preserving its dynamic typestd::current_exception()exception_ptr; how you move one across a threadstd::rethrow_exception(p)the other end of that

The four exception-safety guarantees

GuaranteePromiseTypical example
No-throwwill not throw at alldestructors, swap, move ctors
Strongeither it succeeded, or nothing changedvector::push_back
Basicinvariants hold, no leaks; state unspecifiedthe default you should always meet
Noneanything may have happeneda bug

noexcept is a promise enforced by termination

void f() noexcept;if an exception escapes, std::terminate — no unwinding, no catchnoexcept(expr)the operator: a compile-time bool, true if expr cannot throwvoid f() noexcept(noexcept(g()));conditional — propagate g's promise~T()implicitly noexcept since C++11; throwing from one during unwinding terminates
Where noexcept genuinely matters: move constructors and move assignment (or vector copies instead — see the moves card), swap, destructors, and the hash and comparison functions of unordered containers. Elsewhere it is a constraint on your future self with little payoff, because it cannot be relaxed without breaking callers.

RAII is the only correct cleanup

void bad() { auto* p = new T; use(p); delete p; } // leaks if use() throws void good() { auto p = std::make_unique<T>(); use(*p); } // cannot leak { std::lock_guard lk{m}; … } // unlocks on every path, throw included { std::ofstream f{"x"}; … } // closed on every path

The standard hierarchy

std::exceptionbase; what() is the only member you get  logic_errorinvalid_argument, domain_error, length_error, out_of_range  runtime_errorrange_error, overflow_error, underflow_error, system_error  bad_allocthrown by new; new(std::nothrow) returns null instead  bad_cast / bad_typeidfrom dynamic_cast to a reference / typeid of a null  bad_optional_access / bad_variant_accessoptional::value(), std::get

When not to use exceptions, and what instead

std::optional<T>absence is normal and needs no explanationstd::expected<T,E>C++23: failure is normal and carries a reason; and_then, transform, or_elsestd::error_codean OS error, where the caller decides whether it is exceptional-fno-exceptionsthe embedded/game dialect: throw becomes abort; the whole library must be rebuilt
The measurable rule. Exceptions are for the path you do not test on every call — they are free until they fire. A parse function that fails on a third of its inputs should return expected; one that fails on malformed configuration files should throw.

Smart Pointers

what each one allocates
sizeofAllocationsCopyUse for
unique_ptr<T>81 (the object)move onlythe default — sole ownership
unique_ptr<T,D>8 + sizeof(D)1move onlya custom deleter; stateless D costs nothing (EBO)
shared_ptr<T>162, or 1 with make_sharedatomic refcountgenuinely shared ownership
weak_ptr<T>160breaking cycles; caches; observers
raw T*80non-owning observation — a perfectly good parameter type

The control block

shared_ptr control block object +0 T* ------------------------------------------> [ T ] +8 ctl -----> [ strong count | weak count | deleter | allocator ]

The object is destroyed when strong hits zero; the control block is freed when weak also hits zero. make_shared puts the object and the control block in one allocation — faster, one cache line — at the cost that a surviving weak_ptr keeps the object's storage (not the object) alive. For a large object with long-lived weak references, that is a reason to prefer the two-allocation form.

make_unique<T>(args…)C++14; exception-safe, no repeated type namemake_shared<T>(args…)one allocation; cannot supply a custom deletermake_unique_for_overwrite<T>C++20; default-init, skips the zeroing for a big bufferallocate_sharedmake_shared with your allocator
Never build two shared_ptrs from the same raw pointer. shared_ptr<T> a{p}; shared_ptr<T> b{p}; creates two control blocks and deletes the object twice. If a class needs to hand out a shared_ptr to itself, derive from std::enable_shared_from_this<T> and call shared_from_this() — and only ever after a shared_ptr already owns it, or you get bad_weak_ptr.

The aliasing constructor

auto whole = std::make_shared<Record>(); std::shared_ptr<Field> part(whole, &whole->field); // shares WHOLE's refcount, points at the member

The member stays valid as long as any of these live. This is how you hand out a reference to part of a shared object without a second ownership scheme.

Passing them — the parameter table

ParameterSays
const T& / T*I only look at it. The common case.
unique_ptr<T> by valueI take ownership. Caller must std::move.
shared_ptr<T> by valueI share ownership and may outlive you. Costs an atomic pair.
const shared_ptr<T>&I might copy it. Usually a sign the parameter should be const T&.
unique_ptr<T>&I may reseat it. Rare and worth a comment.
shared_ptr is thread-safe about its refcount only. Copying a shared_ptr from several threads is safe; writing through it, or reassigning the same shared_ptr object from two threads, is not. The pointee needs its own synchronisation.

Containers

complexity, layout and iterator invalidation
ContainerAccessInsertEraseFindMemory
vectorO(1)O(1) amortised at the backO(n) in the middleO(n)contiguous — the default
array<T,N>O(1)O(n)on the stack, no indirection
dequeO(1)O(1) at both endsO(n) middleO(n)chunked; pointer-stable at the ends
listO(n)O(1) given an iteratorO(1)O(n)2 pointers/node — almost never worth it
forward_listO(n)O(1) afterO(1) afterO(n)1 pointer/node; no size()
map/setO(log n)O(log n)O(log n)red-black tree; ordered, stable
unordered_map/setO(1) averageO(1) averageO(1) averagechained buckets — a pointer chase per lookup
flat_map (C++23)O(1)O(n)O(n)O(log n)two sorted vectors; cache-friendly, read-mostly
priority_queuetop O(1)O(log n)O(log n)a heap over a vector; max-heap by default
Use vector until you have measured otherwise. A linear scan of a contiguous array beats a tree or a hash for small n by a margin that surprises people — the crossover against map is often in the hundreds of elements, because every node is a cache miss. std::list is almost never the right answer; its O(1) splice is the one case that is.

Iterator, pointer and reference invalidation

ContainerInsert invalidatesErase invalidates
vectoreverything if it reallocates; else from the insertion point onfrom the erase point on
dequeall iterators; references survive if you insert at an endall, unless at an end
list/forward_listnothingonly the erased element
map/set (all four)nothingonly the erased element
unordered_*iterators on rehash; references neveronly the erased element
The two that bite. (1) deque insertion invalidates every iterator even though it does not move the elements — iterators must know the chunk map. (2) Any vector operation that can reallocate invalidates everything, so v.push_back(v[0]); is undefined unless the implementation takes the copy first — which the standard does require for this case, but not for the general v.push_back(f(v[0])). Reserve, or take the copy yourself.

Erasing correctly

for (auto it = v.begin(); it != v.end(); ) // note: no ++it in the header if (pred(*it)) it = v.erase(it); else ++it; // erase RETURNS the next valid iterator std::erase_if(v, pred); // C++20 -- one line, and O(n) not O(n^2) v.erase(std::remove_if(v.begin(), v.end(), pred), v.end()); // the pre-C++20 spelling

remove_if does not remove anything — it partitions and returns the new logical end. Forgetting the second erase leaves the tail intact and the size unchanged. That is the erase-remove idiom, and std::erase_if exists because everyone got it wrong.

Node handles and the ops that avoid a copy

m.extract(k)C++17: detach a node — move a key between maps without reallocatingm.merge(other)splice all compatible nodes acrossm.try_emplace(k, args…)constructs the value only if the key is absent — unlike emplacem.insert_or_assign(k, v)says which it did; operator[] default-constructs firstm.contains(k)C++20; better than count or find != end
map::operator[] inserts. if (m[k] == 0) on a missing key default-constructs an entry and grows the map. It is also not available on a const map, which is how you find out. Use at(), find() or contains() to read.

Iterators, Ranges and Algorithms

the C++20 rearrangement

Iterator categories, and what each guarantees

inputread once, single pass — istream_iteratoroutputwrite once, single pass — back_inserterforwardmulti-pass, ++ only — forward_listbidirectionaladds --list, maprandom accessadds +n, -, [], ordering — dequecontiguousC++17: and the elements are adjacent in memory — vector, array, string, span

C++20 respecified all six as concepts and decoupled the iterator from its sentinel, so a range's end need not be the same type as its begin — which is what makes an infinite range, or a null-terminated C string as a range, expressible at all.

Ranges: algorithms that take the container

std::sort(v.begin(), v.end()); // classic std::ranges::sort(v); // C++20 std::ranges::sort(v, {}, &Person::age); // comparator, then a PROJECTION

The projection argument is the quiet win: it applies to each element before the comparator, so sorting by a member needs no lambda at all.

Views — lazy, composable, non-owning

auto r = v | std::views::filter([](int n){ return n % 2 == 0; }) | std::views::transform([](int n){ return n * n; }) | std::views::take(5); // nothing has been computed yet; the work happens as you iterate r
ViewDoes
filter, transform, take, dropthe basics
take_while, drop_whilepredicate-bounded
reverse, join, splitrestructuring
iota, repeat (C++23)generated; iota(0) is infinite
enumerate, zip, adjacent, chunk, slide (C++23)the ones people had been writing by hand
elements, keys, valuestuple/pair projection — m | views::keys
ranges::to<C>() (C++23)materialise a view into a real container
A view does not own its elements. auto bad = make_vector() | views::filter(p); leaves bad dangling — the temporary vector died at the end of the statement. Name the container first. views::owning_view and the borrowed_range concept exist to make some of these a compile error, but not all of them.
filter's begin() is not O(1) and caches. It must scan for the first match, so a filtered view is not a forward_range in the cheap sense and iterating it twice is not free. It is also why filter | reverse often does not compile.

The algorithms you should stop hand-writing

find / find_if / count_ifthe loop you were about to writeall_of / any_of / none_ofpredicate over a rangetransform / for_eachmap, in place or into an output iteratoraccumulate / reduce / transform_reducereduce may reorder — only for associative ops; takes an execution policysort / stable_sort / partial_sort / nth_elementnth_element is O(n) — use it for a medianlower_bound / upper_bound / equal_range / binary_searchsorted input assumed, not checkedrotate / partition / stable_partition / shufflerearrangement without a temporaryuniqueadjacent duplicates only — sort firstset_union / set_intersection / set_differenceon sorted ranges, not on std::set

Execution policies (C++17)

std::execution::seqsequentialparparallel; your callable must be thread-safepar_unseqparallel and vectorised; no locks, no allocation in the callableunseqC++20: vectorised, one thread

libstdc++ needs Intel TBB linked (-ltbb) for these to be anything but sequential; libc++ support is newer still. Check before you assume you got parallelism.

Strings, Views and Formatting

SSO, and the lifetime trap

Small string optimisation

Implementationsizeof(std::string)Inline capacity
libstdc++ (GCC)3215 chars
libc++ (Clang/Apple)2422 chars
MSVC3215 chars

Under the inline capacity there is no allocation at all — which is why passing short strings by value is cheaper than people expect, and why a std::string member makes a class much bigger than a pointer.

string_view: a pointer and a length

void f(std::string_view s)the right parameter type for "I only read it" — binds to string, literal, char*, all with no copys.substr(2,3)O(1) — no allocation, unlike string::substrs.data()not necessarily null-terminated — never hand it to a C APIstd::string{sv}the explicit copy, when you need to own it
The dangling string_view.
std::string_view sv = get_name(); // returns std::string BY VALUE use(sv); // the temporary died at the semicolon -- UB std::string_view f() { std::string s = …; return s; } // returns a view of a dead local
A string_view member of a class is a lifetime contract you have to document. As a parameter it is safe and excellent; as a return type or a member, think twice. -Wdangling-gsl catches the easy cases.

std::format (C++20) and std::print (C++23)

std::format("{} scored {:.2f}%", name, pct); // returns a std::string std::format("{0} {1} {0}", a, b); // positional std::format("{:>10}|{:^10}|{:<10}", …); // right / centre / left in 10 columns std::format("{:#010x} {:+.3e} {:b}", n, d, bits); // 0x-prefixed, signed exponent, binary std::print("{}\n", v); // C++23 -- no iostream, no allocation std::println("{}", v); // and the newline for you

The format string is checked at compile time — a wrong placeholder count or an inapplicable specifier is a compile error, not a run-time surprise the way printf is. Specialise std::formatter<T> to make your own type formattable; C++23 formats ranges and tuples out of the box.

Conversions, without the old traps

std::from_chars(b, e, v)C++17: no locale, no allocation, no exceptions — returns an error code. The fast one.std::to_chars(b, e, v)the other direction; shortest round-trip representation for floatsstd::stoi / stodthrows invalid_argument / out_of_range; locale-dependentatoiundefined on overflow and silently returns 0 on garbage — do not use itstd::to_stringconvenient; locale-independent since C++17 but slower than to_chars

The rest of the string surface

starts_with / ends_withC++20, on both string and string_viewcontainsC++23s.resize_and_overwrite(n, op)C++23: fill a buffer without paying for the zeroing<charconv> / <string_view> / <format>the three headers this card lives inchar8_t / u8""C++20 made UTF-8 literals a distinct type — a breaking change from C++17

Lambdas and Callables

a struct with an operator()

A lambda is sugar for a unique unnamed class with an operator(). Knowing that answers every question about what it costs and what it captures.

int y = 10; auto f = [y](int x) { return x + y; }; // the compiler writes, near enough: struct __lambda { int y; int operator()(int x) const { return x + y; } }; __lambda f{10};

Capture

[x]by value — copied at the point the lambda is created, not called[&x]by reference — a dangling reference if the lambda outlives x[=]everything used, by value. Captures this as a raw pointer[&]everything used, by reference[this]the pointer — members are then accessed through it, by reference[*this]C++17: a copy of the whole object — what you want for an async callback[p = std::move(ptr)]C++14 init-capture: move into the closure[…args]pack capture; […args = std::move(args)] to move a pack in
[=] does not capture members by value. It captures this, so [=]{ return member_; } holds a raw pointer to the object and dangles the moment the object dies — a classic use-after-free in any callback that outlives its owner. C++20 deprecates the implicit this capture in [=]; write [*this] or [member_ = member_].

The modifiers

mutableoperator() stops being const — you may modify by-value capturesconstexprC++17; implicit when the body qualifiesconstevalC++20staticC++23: no captures, and no implicit object parameter — a slightly cheaper callnoexceptas on any function-> Texplicit return type; needed when the deduced one is wrong (references, or two return statements)

Generic and templated lambdas

auto g = [](auto x, auto y) { return x + y; }; // C++14 auto h = []<class T>(std::vector<T>& v) { return v.size(); }; // C++20 -- names T auto fw = []<class… A>(A&&… a) { return f(std::forward<A>(a)…); }; // perfect forwarding

Recursion, three ways

std::function<int(int)> fact = [&](int n){ return n<2 ? 1 : n*fact(n-1); }; // allocates, indirect auto fact2 = [](this auto&& self, int n) { return n<2 ? 1 : n*self(n-1); }; // C++23 -- free auto fact3 = [](auto&& self, int n) -> int { return n<2 ? 1 : n*self(self,n-1); }; // the old trick

Conversion to a function pointer

A captureless lambda converts implicitly to a plain function pointer — which is how you pass one to a C callback. Any capture at all and it cannot, because there is state to carry.

void (*cb)(int) = [](int x){ … };fine — no capturesqsort(…, [](const void* a, const void* b){ … });the standard reason this matters+[]{…}the unary-plus trick that forces the conversion, when you need the pointer type deduced

The callable menagerie

std::function<R(A)>owning type erasure; heap allocates if the target is large; copyablestd::move_only_functionC++23 — the one you usually wanted; no copy, so no allocation for a moved-in lambdastd::function_refC++26 — non-owning, for a parameter you only callstd::invoke(f, args…)calls anything, including a pointer-to-member, uniformlystd::bind_front(f, a)C++20; bind_back in C++23. Both beat std::bind, which you should not use

Concurrency and the Memory Model

threads, and what the hardware may reorder
std::jthread t{f};C++20 — joins in its destructor and carries a stop_token. Use this one.std::thread t{f}; t.join();if a std::thread is destroyed still joinable, std::terminatestd::async(std::launch::async, f)future; whose destructor blocks — the notorious wartstd::packaged_task / promise / futurethe manual plumbing

Mutual exclusion

std::lock_guard lk{m};scoped, no options, zero overheadstd::unique_lock lk{m};movable, deferrable, works with a condition variablestd::scoped_lock lk{m1,m2};C++17 — locks several deadlock-free; the default for >1std::shared_lock lk{sm};reader half of shared_mutexstd::call_once / once_flagexactly-once init; a function-local static already does thisstd::counting_semaphore / latch / barrierC++20
Condition variables need the predicate form. cv.wait(lk) can return without a notify — a spurious wakeup, and a real one on POSIX. Always cv.wait(lk, []{ return ready; });, and always modify the predicate's state while holding the mutex, or you race with the wait.

Atomics and the six memory orderings

OrderGuaranteesUse
relaxedatomicity only; no ordering with other variablescounters you only read at the end
consumedata-dependent ordering — every implementation promotes it to acquiredo not use
acquireno later read/write moves before this loadthe reader half of a handoff
releaseno earlier read/write moves after this storethe writer half
acq_relboth, for a read-modify-writefetch_add on a lock
seq_csta single total order across all seq_cst ops — the defaultwhen you are not certain, which is most of the time
std::atomic<bool> ready{false}; Data data; // producer data = make(); ready.store(true, std::memory_order_release); // consumer while (!ready.load(std::memory_order_acquire)) {} use(data); // guaranteed to see the write to data -- release/acquire pairs on the SAME variable
x86 gives you acquire/release for free — only seq_cst stores emit a fence (an xchg or mfence). On AArch64 the difference is real and measurable. Write seq_cst first, then relax only what a benchmark says is hot, and only with a model in your head you could defend at a whiteboard.

What the standard actually promises

data racetwo threads access the same location, one writes, no happens-before → undefined behaviour, not a garbled valueatomic<T>::is_lock_free()run-time; is_always_lock_free is the constexpr oneatomic_ref<T>C++20: atomic operations on an object you do not ownatomic<shared_ptr<T>>C++20; replaced the free atomic_load(shared_ptr*) overloadswait / notify_oneC++20: futex-style blocking on an atomic, no mutexthread_localone instance per thread; construction on first use, destruction at thread exit

Coroutines (C++20), briefly

A function containing co_await, co_yield or co_return is a coroutine: the compiler splits it into a state machine and heap-allocates the frame (elidable, and often elided). The standard ships only the machinerycoroutine_handle, the promise-type protocol — and no usable types. C++23 adds std::generator; for anything else you are using cppcoro, libunifex, or asio, until std::execution lands in C++26.

Undefined Behaviour

the catalogue that actually bites

UB is not "it might crash". The optimiser is permitted to assume UB never happens, and then to delete code that would only run if it did. That is why a UB bug so often manifests as a security hole rather than a fault.

int f(int* p) { int v = *p; // the compiler now KNOWS p is non-null... if (!p) return 0; // ...so it deletes this check entirely return v; }

The list, in rough order of how often it costs an evening

UBSymptomCaught by
use after free / after moveworks until it doesn't-fsanitize=address
reading an uninitialised valuenondeterminism, changes under -O2-fsanitize=memory (Clang), -Wmaybe-uninitialized
signed integer overflowloops the optimiser proved could not end-fsanitize=signed-integer-overflow
out-of-bounds indexsilent corruption; v[i] is unchecked, v.at(i) is notASan; -D_GLIBCXX_ASSERTIONS
invalidated iteratorsee the containers card-D_GLIBCXX_DEBUG, _LIBCPP_HARDENING_MODE
data raceanything at all-fsanitize=thread
strict-aliasing violationa store the compiler reorders past a load-Wstrict-aliasing=2; use std::bit_cast
misaligned loadfine on x86, a fault on some Arm-fsanitize=alignment
missing return in a non-void functionfalls into whatever is next-Wreturn-type — make it an error
dangling reference to a temporaryworks in debug, fails in release-Wdangling-gsl, ASan
deleting through a base without a virtual dtorpartial destruction, leak-Wdelete-non-virtual-dtor
modifying an object twice in one expressioni = i++-Wsequence-point; C++17 fixed some of these
infinite loop with no side effectsdeleted outrightnothing — know the rule

The flags to build with

-Wall -Wextra -Wpedanticthe floor-Wshadow -Wconversion -Wsign-conversionnoisy and worth it on new code-Wnon-virtual-dtor -Woverloaded-virtual -Wold-style-castthe C++-specific ones people forget-Werror=return-typepromote the one that is always a bug-fsanitize=address,undefined -fno-omit-frame-pointerthe debug build; ~2× slower-fsanitize=threadseparate build — incompatible with ASan-D_GLIBCXX_ASSERTIONSlibstdc++ bounds checks at ~no cost; -D_GLIBCXX_DEBUG is the heavyweight version-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVEthe libc++ equivalent-fstack-protector-strong -D_FORTIFY_SOURCE=2ship with these
Run the test suite under ASan+UBSan in CI. It is the single highest-value change most C++ codebases can make: those two together find the majority of the table above, and they find it at the line that caused it rather than three functions later.

Modules, Build and Tooling

and the C++11 → C++26 timeline

Modules (C++20) — the state of it

// math.cppm export module math; export int add(int a, int b) { return a + b; } // exported int helper(); // not // main.cpp import math; import std; // C++23 -- the whole standard library, one import

A module is compiled once into a binary interface (BMI) and imported, so no textual re-parsing and no macro leakage. The build-system problem is that modules must be built in dependency order, which the compiler can only discover by scanning — hence -fmodules-ts era pain. CMake 3.28+ supports them for Ninja and MSVC; GCC 15 and Clang 21 both build import std;, but it is still not something to reach for in a codebase you need to ship this quarter.

What the preprocessor still does, and the guard

#pragma onceuniversally supported, not standard; fails across bind-mounted duplicate paths#ifndef X_H / #define X_H / #endifstandard, verbose, always correct#include <a> vs "a"angle: system paths only; quotes: the including file's directory first__has_include(<x>)C++17 — feature-test a header__cpp_lib_rangesthe feature-test macros in <version>; test these, not __cplusplus

CMake, the parts you actually need

cmake_minimum_required(VERSION 3.28) project(app CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # clangd and clang-tidy read this add_library(core src/a.cpp) target_include_directories(core PUBLIC include) # PUBLIC propagates to consumers target_compile_features(core PUBLIC cxx_std_23) add_executable(app src/main.cpp) target_link_libraries(app PRIVATE core)
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfoconfigurecmake --build build -jbuildctest --test-dir build --output-on-failuretestPUBLIC / PRIVATE / INTERFACEpropagate to consumers / this target only / consumers only

The rest of the toolbox

clang-formatthe argument-ender; a .clang-format in the repo rootclang-tidythe linter — modernize-*, bugprone-*, performance-*, and it can --fixclangdthe language server; needs compile_commands.jsoninclude-what-you-useprunes the header graph, which is where your build time wentccache / sccachethe other place your build time wentgodbolt.orgCompiler Explorer — the fastest way to answer "what does this compile to"perf / Instruments / vtuneprofile before optimising, every timevalgrind --tool=memcheckslower than ASan, finds a different set; still worth a run

What arrived when

StandardThe things you actually use
C++11auto, lambdas, move semantics, nullptr, range-for, unique_ptr/shared_ptr, constexpr, variadic templates, <thread>, <atomic>, the memory model, enum class, override/final, = delete
C++14make_unique, generic lambdas, init-capture, relaxed constexpr, variable templates
C++17structured bindings, if constexpr, guaranteed copy elision, optional/variant/any, string_view, filesystem, CTAD, fold expressions, parallel algorithms, <charconv>, inline variables
C++20concepts, ranges, <format>, <=>, coroutines, modules, span, jthread, latch/barrier/semaphore, designated initialisers, consteval/constinit, <bit>, source_location, calendar/timezone <chrono>
C++23std::expected, std::print/println, deducing this, mdspan, flat_map/flat_set, generator, zip/enumerate/chunk, ranges::to, move_only_function, import std;, multidimensional operator[]
C++26static reflection, contracts, std::execution (senders/receivers), std::hive, function_ref, constexpr exceptions, erroneous (rather than undefined) uninitialised reads, #embed
Check before you rely on it. gcc -std=c++23 -x c++ -E -dM - < /dev/null | grep __cpp_lib lists the library feature-test macros your toolchain actually implements, which is a very different list from what the standard says. cppreference's compiler support table is the other half of the answer.

C++ on Embedded Targets

the freestanding subset

This is the one domain where C++ has no real competitor: it is the only language in wide use that will inline a type-safe abstraction over a memory-mapped register down to the same single store instruction you would have written by hand. Everything below is about keeping that property while giving up the parts of the runtime that assume an operating system.

Hosted vs freestanding

The standard defines two implementation kinds. A freestanding one need only supply the headers that do not require an OS — and until C++23 that list was almost useless. P1642 fixed it: the C++23 freestanding subset includes most of <type_traits>, <concepts>, <utility>, <array>, <span>, <bit>, <atomic>, <ratio> and much of <algorithm>.

FeatureCost if you keep itVerdict on a small MCU
templates, constexpr, inlinezerokeep — this is the whole point
classes, references, overloading, namespaceszerokeep
lambdas (captureless or small)zerokeep
virtual+1 vptr per object, +vtable in flash, no inliningfine in moderation; not in an ISR
exceptions~10–100 KB of unwind tables and personality routineusually -fno-exceptions
RTTIa type_info per polymorphic classusually -fno-rtti
<iostream>~200 KB and a static-init stormnever — and merely including it pulls it in
std::string, std::vector, std::functionheapavoid; see the fixed-capacity replacements below
std::array, std::span, std::optional, std::bitsetzerokeep — all header-only, no allocation
The economical build.
-Os -ffunction-sections -fdata-sections -Wl,--gc-sections -fno-exceptions -fno-rtti -fno-threadsafe-statics -fno-use-cxa-atexit -fno-unwind-tables -fno-asynchronous-unwind-tables
--gc-sections with the two -f*-sections flags is the big one: it discards every function you did not call, which is what makes a template-heavy header library free.

The four flags that need explaining

-fno-exceptionsthrow becomes a call to abort; try is a syntax error. Not standard C++, and the whole libstdc++ you link must match-fno-rttiremoves typeid and dynamic_cast; static_cast down is your only option and is unchecked-fno-threadsafe-staticsfunction-local statics lose their __cxa_guard_acquire lock — safe only if you never initialise one from two contexts-fno-use-cxa-atexitstops registering destructors for globals; on an MCU nothing ever exits, so the table is dead weight
Static constructors run before main, from the startup code. On a bare-metal target that is __libc_init_array() walking .init_array. If your linker script omits that section, or your startup file never calls it, your global objects are never constructed and every field is zero — a failure that looks exactly like a hardware problem. Check the map file for .init_array before blaming the peripheral.

Do not allocate

The objection to new on an MCU is not speed, it is fragmentation: a long-running device with a few-kilobyte heap will eventually fail an allocation it made successfully a thousand times before, and there is nobody to restart it. Allocate everything statically, at startup, and never free.

Instead ofUse
std::vector<T>std::array<T,N> + a size, or etl::vector<T,N>, or C++26 std::inplace_vector
std::stringstd::array<char,N> + std::string_view, or etl::string<N>
std::functiona template parameter, a function pointer, or etl::delegate / std::function_ref (C++26)
std::mapa sorted std::array + std::lower_bound, built constexpr
new/deleteplacement new into a static buffer, or a fixed-block pool
the heap generallyoverride operator new to call abort(), and let the linker prove nothing calls it
// prove at link time that nothing allocates void* operator new(std::size_t) { std::abort(); } void operator delete(void*) noexcept { std::abort(); } // better: do not define them at all, and let the link fail with // "undefined reference to operator new" naming the culprit

Zero-Cost Hardware Abstraction

registers, volatile and constexpr

The reason to write firmware in C++ rather than C is this card: you can wrap a peripheral in a type that makes the wrong call a compile error, and still emit the same instruction.

volatile — what it does and what it does not

doesforbids the compiler from eliding, reordering or coalescing accesses to that objectdoes notmake anything atomic; does not order accesses against non-volatile ones; does not emit a memory barrier; does not stop the CPU reorderingfor MMIOcorrect and necessaryfor threads or ISRs sharing datawrong — use std::atomic, which on Cortex-M is still a plain load or store
// the canonical register definition -- no macros struct GPIO { volatile std::uint32_t MODER, OTYPER, OSPEEDR, PUPDR, IDR, ODR, BSRR; }; inline GPIO* const gpioa = reinterpret_cast<GPIO*>(0x4002'0000); gpioa->BSRR = 1u << 5; // compiles to one store

The reinterpret_cast is technically not constexpr, which is why the pointer is const rather than constexpr. Every embedded codebase does this; the standard has never quite blessed it, and C++26's reflection does not change it either.

A type-safe pin, that costs nothing

template <std::uint32_t Base, unsigned N> struct Pin { static_assert(N < 16, "STM32 ports have 16 pins"); static void set() { reg()->BSRR = 1u << N; } static void clear() { reg()->BSRR = 1u << (N + 16); } static bool read() { return reg()->IDR & (1u << N); } private: static GPIO* reg() { return reinterpret_cast<GPIO*>(Base); } }; using Led = Pin<0x4002'0000, 5>; Led::set(); // -Os output: str r1, [r0] -- identical to the C macro, and a typo is now a compile error
Check it, do not assume it. arm-none-eabi-g++ -Os -S, or paste it into Compiler Explorer with an arm-none-eabi compiler selected. "Zero-cost" is a claim about a specific compiler at a specific optimisation level, and -O0 will make a liar of you.

Strong types for units, which is where the bugs are

enum class Milliseconds : std::uint32_t {}; enum class Hertz : std::uint32_t {}; void delay(Milliseconds); delay(Milliseconds{500}); // fine delay(500); // compile error -- which is the entire point using namespace std::chrono_literals; constexpr auto period = 20ms; // or use <chrono>: freestanding, constexpr, and free

Compile-time tables: flash, not RAM

constexpr auto sine = []{ std::array<std::int16_t, 256> t{}; for (std::size_t i = 0; i < t.size(); ++i) t[i] = static_cast<std::int16_t>(32767 * std::sin(2 * 3.14159265 * i / 256)); return t; }(); // computed by the COMPILER; lands in .rodata

A constexpr table with no runtime initialiser goes in .rodata and is executed in place from flash — it costs no RAM at all. Drop the constexpr and the same array becomes a .data object that must be copied from flash to RAM at startup. On a 32 KB part, that distinction is the difference between fitting and not. Check with arm-none-eabi-size and the map file.

Interrupt handlers

extern "C" void TIM2_IRQHandler() // extern "C" -- the vector table is C symbols { static std::atomic<std::uint32_t> ticks{0}; ticks.fetch_add(1, std::memory_order_relaxed); }
Three rules for an ISR, and they are not negotiable. (1) extern "C", or name mangling means the linker quietly leaves the weak default handler in the vector table and your interrupt does nothing. (2) No allocation, no exceptions, no printf, no blocking. (3) Share data with std::atomic, never with volatile alone — on Cortex-M a std::atomic<uint32_t> is a plain ldr/str, so it is free, and it is the only thing that actually orders the compiler.

Interrupts and critical sections

class CriticalSection { // RAII, on bare metal std::uint32_t primask_; public: CriticalSection() : primask_(__get_PRIMASK()) { __disable_irq(); } ~CriticalSection() { if (!primask_) __enable_irq(); } // restore, do not blindly enable CriticalSection(const CriticalSection&) = delete; }; { CriticalSection cs; shared = value; } // interrupts off for exactly this block

Saving and restoring PRIMASK rather than unconditionally re-enabling is what makes it safe to nest — the naive version silently enables interrupts inside an outer critical section.

The Real Embedded Platforms

what each toolchain actually gives you
PlatformToolchainC++ levelReality
Arduino AVR (UNO R3, Nano)avr-gccC++17no <thread>, no exceptions, tiny libstdc++; 2 KB RAM. String fragments the heap — use char[]
Arduino UNO R4 (RA4M1 + ESP32-S3)arm-none-eabi-gccC++17Cortex-M4F, 32 KB RAM; comfortable for real C++. See the UNO R4 sheet
ESP32 / S3 / C3ESP-IDF (xtensa- or riscv32-esp-elf gcc)C++23 (IDF 5.x)FreeRTOS underneath, so std::thread and std::mutex work; exceptions off by default, switchable in menuconfig. See the ESP32 sheet
Raspberry Pi Picopico-sdk + arm-none-eabiC++17/20CMake-native; two cores, and the SDK is C with C++ wrappers
Zephyr RTOSwest + Zephyr SDKC++20C++ is a first-class option but the kernel API is C; no exceptions by default
Raspberry Pi 5 / Orange Pithe distro's g++whatever GCC shipsfull Linux — this is hosted C++, not embedded C++. The whole standard library is yours
Mbed / STM32Cubearm-none-eabiC++14–17HAL is C; Cube generates C. Wrap it, do not fight it
The distinction that matters. A Pi 5 running Linux is a small computer, and ordinary C++ is correct there — heap, threads, exceptions, iostreams and all. A Cortex-M with 32 KB of RAM is a different discipline. Do not carry the MCU habits onto the Pi, and definitely do not carry the Pi habits onto the MCU.

Arduino is C++, with the parts hidden

// a .ino is C++ with a prologue: the IDE prepends #include <Arduino.h>, // generates forward declarations for your functions, and appends int main() { init(); setup(); for (;;) { loop(); if (serialEventRun) serialEventRun(); } }
arduino-cli compile --fqbn arduino:renesas_uno:unor4wifi .build from the shellarduino-cli compile --show-propertiesthe real compiler command line, including the C++ standardcompiler.cpp.extra_flags=-std=gnu++17in platform.local.txt, to raise itrename .ino to .cppand you have ordinary C++ with none of the preprocessing magic

Sizing the result — the numbers to watch

arm-none-eabi-size -A firmware.elf.text = flash, .data = flash and RAM, .bss = RAMarm-none-eabi-nm --size-sort -S firmware.elf | tail -20the twenty biggest symbols; usually one printf family-Wl,-Map=out.mapand grep it for .init_array, malloc, _ZTV (vtables)-Wl,--print-memory-usagea percentage-of-region summary at every linkarm-none-eabi-objdump -dwhen you need to see whether the template really did inline
printf("%f") pulls in the whole soft-float formatting engine — typically 10–25 KB, and it is the single most common reason a firmware image will not fit. Use -u _printf_float only when you truly need it, prefer integer formatting or fixed-point, and on newlib-nano keep --specs=nano.specs. std::to_chars is a much cheaper float formatter where it is available.

Libraries worth knowing about

ETL (Embedded Template Library)the standard containers with fixed capacity and no heap — the single most useful third-party library here{fmt}std::format before your toolchain has it; compiles small with FMT_STATIC_THOUSANDS_SEPARATORCMSIS / CMSIS-DSPArm's own headers: __disable_irq, atomics intrinsics, optimised DSP kernelsKvasir / modm / libopencm3constexpr register abstraction taken seriouslyCatch2 / doctest / GoogleTestrun the logic half of your firmware on the host, where a debugger is pleasant
The highest-value habit. Split the firmware into a hardware layer and a logic layer, template the logic over the hardware interface, and compile the logic for the host with a mock. You get a fast test loop, ASan and UBSan on your algorithms, and no JTAG probe — and because it is templated, the target build still inlines everything and costs nothing.

Standard Library Index

Every container, algorithm, view, trait and header worth remembering, grouped by what it does — type in the filter box, or press /DotsC++17 or earlierC++20C++23C++26deprecated

Containers

36

Sequence

std::array<T,N>fixed size, no indirection
std::vector<T>contiguous, growable
std::deque<T>O(1) at both ends
std::list<T>doubly linked
std::forward_list<T>singly linked
std::vector<bool>a bitfield, not a container
std::inplace_vector<T,N>fixed capacity, dynamic size
std::hive<T>stable-reference bucket container

Associative — ordered (red-black tree)

std::map<K,V>sorted, unique keys
std::multimap<K,V>sorted, duplicate keys
std::set<K>sorted unique values
std::multiset<K>sorted, duplicates allowed
transparent comparatorstd::less<> enables heterogeneous lookup

Associative — unordered (hash)

std::unordered_map<K,V>hashed, unique keys
std::unordered_set<K>hashed unique values
std::hash<T>the default hash
reserve / max_load_factorcontrol rehashing
heterogeneous lookupneeds is_transparent on hash AND equal

Adaptors & flat containers

std::stack<T>LIFO over a deque
std::queue<T>FIFO over a deque
std::priority_queue<T>a heap; MAX-heap by default
std::flat_map / flat_setsorted vectors
std::flat_multimap / flat_multisetthe duplicate-key forms

Views over memory

std::span<T>pointer + length over contiguous data
std::span<T,N>static extent
std::mdspan<T,E>multidimensional view
std::string_viewnon-owning string
std::initializer_list<T>a braced list

Common member functions

size / empty / clearthe basics
reserve / capacity / shrink_to_fitvector and string only
emplace_back / emplaceconstruct in place
try_emplace(k, args…)do not construct if the key exists
insert_or_assign(k, v)and tell me which
extract / mergemove nodes between containers
contains(k)membership test
std::erase / erase_iffree functions on any container

Algorithms

40

Non-modifying

find / find_if / find_if_notfirst match
count / count_ifhow many
all_of / any_of / none_ofquantifiers
for_each / for_each_napply
mismatch / equalcompare two ranges
search / find_end / find_first_ofsubsequence search
adjacent_findfirst equal neighbours
lexicographical_comparedictionary order
lexicographical_compare_three_waythe <=> form

Modifying

copy / copy_if / copy_n / copy_backwardcopy
move / move_backwardmove elements
transformmap
fill / fill_n / generate / generate_nwrite values
replace / replace_if / replace_copysubstitute
remove / remove_ifpartition, do NOT erase
unique / unique_copycollapse ADJACENT duplicates
reverse / rotate / shufflerearrange
swap / iter_swap / swap_rangesexchange
shift_left / shift_rightslide elements

Partitioning, sorting, searching

sortintrosort; O(n log n), not stable
stable_sortpreserves equal order; O(n log^2 n) or O(n log n) with memory
partial_sort / partial_sort_copytop k, sorted
nth_elementkth in place, O(n) average
is_sorted / is_sorted_untilcheck
partition / stable_partition / partition_pointsplit by predicate
lower_bound / upper_bound / equal_rangebinary search on sorted data
binary_searchmembership only
merge / inplace_mergecombine sorted ranges
set_union / set_intersection / set_differenceon SORTED RANGES
make_heap / push_heap / pop_heap / sort_heapheap operations
min / max / minmax / clampand the _element forms

Numeric & parallel

std::accumulateleft fold, sequential
std::reducefold, may reorder
std::transform_reducemap then fold
std::inner_productsequential dot product
std::partial_sum / inclusive_scan / exclusive_scanprefix sums
std::iotafill with increasing values
std::midpoint / std::lerpoverflow-free midpoint, interpolation
std::gcd / std::lcmC++17
execution::seq / par / par_unseqexecution policies

Ranges & Views

31

Range algorithms (C++20)

std::ranges::sort(r, cmp, proj)the container, plus a projection
ranges::find / find_if / countas the classics, on a range
ranges::for_each / transform / copy
ranges::min / max / minmaxand _element
ranges::begin / end / size / datacustomisation-point objects
ranges::to<C>()materialise a view into a container
ranges::fold_left / fold_rightthe range-based fold

Views — lazy and composable

views::filter(pred)keep matching
views::transform(f)map
views::take(n) / drop(n)prefix / suffix
views::take_while / drop_whilepredicate-bounded
views::reversebackwards
views::joinflatten one level
views::split(delim)split a range
views::iota(a[, b])generated integers
views::keys / values / elements<N>tuple projection
views::commonmake begin and end the same type
views::counted(it, n)n elements from an iterator
views::zip / zip_transformparallel iteration
views::enumerateindex and value
views::adjacent<N> / pairwisesliding tuples
views::chunk(n) / slide(n)fixed blocks / sliding window
views::repeat(v[, n])a constant range
views::cartesian_productall combinations
views::stride(n)every nth element
views::concatappend ranges

Range concepts

std::ranges::rangehas begin and end
sized_range / common_rangehas size / begin and end are the same type
viewcheap to copy, non-owning
borrowed_rangeiterators outlive the range object
viewable_rangemay be turned into a view

Utilities & Vocabulary Types

37

Sum and product types

std::pair<A,B>two values
std::tuple<T…>n values
std::optional<T>a value, or nothing
std::variant<T…>exactly one of these types
std::anyany copyable type
std::expected<T,E>a value, or a reason it is missing
optional::and_then / transform / or_elsemonadic operations

Type traits (<type_traits>)

is_same_v / is_base_of_v / is_convertible_vrelationships
is_integral_v / is_floating_point_v / is_arithmetic_vcategories
is_pointer_v / is_reference_v / is_const_v / is_enum_v / is_class_vcategories
remove_cv_t / remove_reference_t / decay_tstrip qualifiers
remove_cvref_tstrip const, volatile and reference
add_pointer_t / add_lvalue_reference_tadd qualifiers
conditional_t<B,T,F>compile-time ternary
enable_if_t<B,T>SFINAE gate
void_t<T…>the detection idiom
is_trivially_copyable_vmay I memcpy it?
is_nothrow_move_constructible_vwhat vector checks
invoke_result_t<F,A…>the return type of a call
underlying_type_t<E>an enum’s base type

Functional

std::function<R(A…)>owning, copyable type erasure
std::move_only_function<R(A…)>the non-copyable one
std::function_ref<R(A…)>non-owning callable reference
std::invoke(f, args…)call anything uniformly
std::bind_front(f, a…)partial application
std::bindthe old partial application
std::ref / std::crefreference_wrapper
std::less<> / greater<> / plus<>transparent functors

Small but load-bearing

std::swap / std::exchangeexchange values / set and return the old
std::as_constget a const view
std::addressofthe real address
std::launderafter placement new over an object
std::source_locationfile, line, function of the caller
std::to_arrayarray from a C array
std::to_underlying(e)enum to its base type
std::unreachable()this code cannot be reached
std::stacktracea captured stack trace

Memory, Strings, Time & I/O

36

Smart pointers & memory (<memory>)

std::unique_ptr<T[, D]>sole ownership; 8 bytes
std::make_unique<T>(args…)construct one
std::shared_ptr<T>shared ownership; 16 bytes
std::make_shared<T>(args…)one allocation for object + control block
std::weak_ptr<T>non-owning observer
std::enable_shared_from_this<T>hand out shared_ptr to self
std::make_unique_for_overwritedefault-init, skip the zeroing
placement newconstruct at an address
std::pmr::*polymorphic allocators
std::aligned_alloc / operator new(size, align_val_t)over-aligned allocation

Strings & formatting

std::stringowning, SSO
std::string_viewnon-owning
std::format(fmt, args…)compile-time-checked formatting
std::print / printlnformat straight to stdout
std::formatter<T>make your type formattable
std::from_chars / to_charsfast, locale-free conversion
starts_with / ends_withprefix and suffix tests
containssubstring test
std::regexregular expressions

Time (<chrono>)

steady_clockmonotonic — for measuring durations
system_clockwall clock — for timestamps
high_resolution_clockan alias for one of the above
duration<Rep,Period>a time span
duration_cast<T>lossy conversion
1s / 500ms / 2hchrono literals
year_month_day / sys_daysthe calendar
zoned_time / current_zone()time zones
std::format on a time_pointformatted dates

I/O and the filesystem

<iostream>cin, cout, cerr, clog
std::ofstream / ifstream / stringstreamfile and string streams
std::setw / setprecision / hex / fixedmanipulators, in <iomanip>
sync_with_stdio(false)unhook cin/cout from C stdio
std::filesystem::pathportable paths
fs::exists / is_directory / file_size / remove_allqueries and operations
fs::directory_iterator / recursive_directory_iteratorwalk a tree
std::spanstreama stream over a fixed buffer

Concurrency

32

Threads

std::threada thread; must be joined or detached
std::jthreadjoins in its destructor
std::stop_token / stop_sourcecooperative cancellation
std::this_thread::sleep_for / yield / get_id
hardware_concurrency()a hint, possibly 0
std::async(launch::async, f)run and get a future
std::future / shared_future / promise / packaged_taskthe one-shot channel

Locks

std::mutex / recursive_mutex / timed_mutexmutual exclusion
std::shared_mutexmany readers, one writer
std::lock_guardscoped lock, no options
std::unique_lockmovable, deferrable, unlockable
std::scoped_lockseveral mutexes, deadlock-free
std::shared_lockreader lock
std::condition_variablewait for a predicate
std::call_once / std::once_flagexactly once
std::latch / std::barriersingle-use / reusable rendezvous
std::counting_semaphore<N>a counted permit

Atomics & the memory model

std::atomic<T>lock-free where the hardware allows
load / store / exchangethe basic operations
compare_exchange_weak / _strongCAS
fetch_add / fetch_sub / fetch_or …read-modify-write
memory_order_relaxed / acquire / release / acq_rel / seq_cstthe orderings
memory_order_consumenever implemented as specified
std::atomic_ref<T>atomic ops on an ordinary object
atomic<T>::wait / notify_one / notify_allblock on an atomic
std::atomic<std::shared_ptr<T>>atomic shared pointer
std::atomic_flagthe only always-lock-free type

Coroutines & the future

co_await / co_yield / co_returnsuspend, yield, finish
std::coroutine_handle<P>the frame handle
promise_typethe customisation protocol
std::generator<T>the first usable coroutine type
std::executionsenders and receivers

Embedded & Freestanding

40

Compiler flags that shrink the image

-Os / -Ozoptimise for size
-ffunction-sections -fdata-sectionsone section per symbol
-Wl,--gc-sectionsdrop unreferenced sections
-fno-exceptionsno throw, no unwind tables
-fno-rttino typeid, no dynamic_cast
-fno-threadsafe-staticsno guard variable on local statics
-fno-use-cxa-atexitdo not register global destructors
-fno-unwind-tables -fno-asynchronous-unwind-tablesdrop .eh_frame
--specs=nano.specsnewlib-nano
-Wl,--print-memory-usageregion usage at link time
-Wl,-Map=out.mapthe map file
-mcpu / -mthumb / -mfloat-abi=hardtarget selection

Freestanding-safe library (C++23 P1642)

<type_traits> <concepts> <limits>fully freestanding
<array> <span> <bit> <ratio>freestanding
<utility> <tuple> <optional> <variant>mostly freestanding
<atomic>freestanding
<algorithm> <numeric>largely usable
<chrono>durations are freestanding
<charconv>to_chars / from_chars
<bitset>fixed-size bit array

Do not use on a small MCU

<iostream>~200 KB and a static-init storm
std::string / std::vectorheap, and fragmentation
std::functionmay heap-allocate
std::regexenormous and slow
new / delete / mallocfragmentation with no way to recover
std::shared_ptratomics plus a control-block allocation
printf("%f")10-25 KB of soft-float formatting
exceptions in an ISRno unwinder, no stack to unwind into

Idioms and the toolchain

volatile struct + reinterpret_castmemory-mapped registers
template<addr, pin> struct Pinzero-cost typed GPIO
constexpr lookup tablescomputed at build time, live in flash
enum class as a unitMilliseconds, Hertz
extern "C" void XXX_IRQHandler()interrupt vectors
RAII critical sectionsave and restore PRIMASK
.init_array / __libc_init_arraywhere global constructors run
arm-none-eabi-size -Aflash and RAM by section
arm-none-eabi-nm --size-sort -Sthe biggest symbols
ETL (Embedded Template Library)fixed-capacity STL
CMSISArm’s core headers
host-side unit teststemplate the logic over the hardware

Standard Headers

53

Language support

<cstddef>size_t, ptrdiff_t, nullptr_t, byte
<cstdint>int32_t and friends
<limits>numeric_limits<T>
<climits> <cfloat>the C macros
<typeinfo>type_info, bad_cast
<initializer_list>braced-list support
<compare>the ordering types
<version>every library feature-test macro
<source_location>caller file, line and function
<coroutine>coroutine_handle, the promise protocol
<stdfloat>float16_t, float32_t, bfloat16_t
<meta>static reflection

General utilities

<utility>move, forward, swap, exchange, pair
<tuple>tuple, tie, apply, make_from_tuple
<type_traits>the whole trait set
<functional>function, invoke, ref, less<>, hash
<memory>unique_ptr, shared_ptr, allocators
<optional> <variant> <any>the C++17 vocabulary types
<expected>expected<T,E>
<bitset>fixed-size bit array
<bit>bit_cast, popcount, rotl, endian
<chrono>durations, clocks, and the C++20 calendar
<ratio>compile-time rationals
<scoped_allocator>nested allocator propagation

Strings, containers, iterators, algorithms

<string> <string_view>the string types
<charconv>to_chars, from_chars
<format>std::format
<print>std::print, std::println
<array> <vector> <deque> <list> <forward_list>sequence containers
<map> <set> <unordered_map> <unordered_set>associative containers
<stack> <queue>adaptors
<flat_map> <flat_set>the sorted-vector containers
<span>span<T>
<mdspan>multidimensional span
<iterator>iterator traits, adaptors, back_inserter
<algorithm>the algorithm set
<numeric>accumulate, reduce, iota, gcd, midpoint
<ranges>ranges and views
<concepts>the core concepts
<generator>std::generator

I/O, system, concurrency, diagnostics

<iostream> <fstream> <sstream> <iomanip>the stream family
<filesystem>paths and directory operations
<spanstream>a stream over a fixed buffer
<thread> <mutex> <shared_mutex> <condition_variable>threading
<future> <atomic>futures and atomics
<stop_token> <latch> <barrier> <semaphore>C++20 coordination
<execution>senders and receivers
<exception> <stdexcept> <system_error>the exception types
<cassert>assert
<stacktrace>captured stack traces
<random>engines and distributions
<cmath> <complex> <valarray>numerics
<linalg>BLAS-shaped linear algebra

Standard Concepts

37

Core language concepts (<concepts>)

std::same_as<T,U>exactly the same type
std::derived_from<D,B>publicly and unambiguously derived
std::convertible_to<F,T>implicitly and explicitly convertible
std::common_with / common_reference_withhave a common type
std::integral / signed_integral / unsigned_integralinteger categories
std::floating_pointfloat, double, long double
std::assignable_from<L,R>assignment is valid
std::swappable / swappable_withranges::swap works
std::destructibledestructor will not throw
std::constructible_from<T,A…>constructible from those arguments
std::default_initializableT{} is valid
std::move_constructible / copy_constructiblemovable / copyable construction

Comparison and object concepts

std::equality_comparable[_with]== and != are valid and consistent
std::totally_ordered[_with]a strict total order
std::three_way_comparable<T,Cat><=> yields at least that category
std::movablemove-constructible, assignable and swappable
std::copyablemovable plus copy
std::semiregularcopyable and default-initializable
std::regularsemiregular and equality_comparable

Callable concepts

std::invocable<F,A…>callable with those arguments
std::regular_invocableand does not change observable state
std::predicate<F,A…>invocable, result usable as bool
std::relation / equivalence_relation / strict_weak_orderbinary predicate shapes

Iterator and range concepts

std::input_or_output_iteratorthe floor: ++ and *
std::input_iterator / output_iteratorreadable / writable, single pass
std::forward_iteratormulti-pass
std::bidirectional_iteratoradds --
std::random_access_iteratoradds +n, -, [], ordering
std::contiguous_iteratorand elements are adjacent in memory
std::sentinel_for<S,I>S can mark the end of I
std::sized_sentinel_for<S,I>and s - i is O(1)
std::indirectly_readable / writabledereference semantics
std::ranges::rangehas begin and end
std::ranges::borrowed_rangeiterators outlive the range object
std::ranges::viewO(1) copyable, non-owning
std::ranges::sized_range / common_rangehas size / same begin and end type
std::ranges::viewable_rangemay be piped into a view

Bits, Numerics & Random

35

<bit> (C++20)

std::bit_cast<To>(from)reinterpret the bits, defined
std::popcount(x)number of set bits
std::countl_zero / countr_zeroleading / trailing zeros
std::countl_one / countr_oneleading / trailing ones
std::rotl(x,n) / rotr(x,n)rotate
std::has_single_bit(x)is it a power of two?
std::bit_ceil / bit_floorround to a power of two
std::bit_width(x)bits needed to represent x
std::endian::nativelittle, big, or neither
std::byteswap(x)reverse the bytes

<cmath> and numeric care

std::fma(a,b,c)fused multiply-add, one rounding
std::hypot(x,y)sqrt(x*x+y*y) without overflow
std::isnan / isinf / isfinite / signbitclassification
std::nextafter / nexttowardthe adjacent representable value
std::fmod / std::remainderdifferent rounding of the quotient
std::lround / llround / trunc / floor / ceilintegral conversions
std::lerp(a,b,t) / std::midpoint(a,b)safe interpolation and midpoint
std::numbers::pi / e / sqrt2the constants
-ffast-mathbreaks IEEE semantics globally

<random> — and why not rand()

std::mt19937 / mt19937_64Mersenne Twister
std::minstd_rand / ranlux48the other engines
std::random_devicenon-deterministic seed
std::seed_seqexpand a seed properly
std::uniform_int_distribution<T>unbiased integers in [a,b]
std::uniform_real_distribution<T>reals in [a,b)
std::normal_distribution / poisson / exponentialthe named distributions
std::shuffle(first, last, gen)unbiased shuffle
std::sample(f, l, out, n, gen)reservoir sampling
std::rand / srandthe C generator

Safe integer handling

std::cmp_equal / cmp_less / cmp_greatercompare across signedness correctly
std::in_range<T>(v)does v fit in T?
std::numeric_limits<T>::max()the bound
__builtin_add_overflowchecked arithmetic
std::add_sat / sub_sat / mul_satsaturating arithmetic
-ftrapv / -fwrapv / -fsanitize=signed-integer-overflowthe three responses to overflow

Compiler Flags & Diagnostics

35

Warnings worth turning on

-Wall -Wextrathe baseline
-Wpedanticstrict standard conformance
-Wshadowa declaration shadows another
-Wconversion -Wsign-conversionimplicit narrowing and sign changes
-Wold-style-castC-style casts
-Wnon-virtual-dtorpolymorphic base without a virtual destructor
-Woverloaded-virtuala derived function hides a base virtual
-Wdelete-non-virtual-dtordelete through a base without one
-Wsuggest-overrideyou overrode without saying so
-Wdeprecated-copyimplicit copy generation is deprecated
-Wrange-loop-constructthe range-for copies each element
-Wdangling-gsla view of a dead temporary
-Wpessimizing-movestd::move that defeats copy elision
-Werror=return-typepromote the one that is always a bug

Sanitizers & hardening

-fsanitize=addressuse-after-free, overflow, leaks
-fsanitize=undefinedthe UB catalogue
-fsanitize=threaddata races
-fsanitize=memoryuninitialised reads
-fno-omit-frame-pointerreadable sanitizer stacks
-D_GLIBCXX_ASSERTIONSlibstdc++ bounds checks
-D_GLIBCXX_DEBUGfull libstdc++ debug containers
-D_LIBCPP_HARDENING_MODE=…libc++ hardening
-fstack-protector-strongstack canaries
-D_FORTIFY_SOURCE=2checked libc calls

Codegen & inspection

-O0 -O1 -O2 -O3 -Os -Ogoptimisation levels
-march=native -mtune=nativethis machine only
-fltolink-time optimisation
-fvisibility=hiddendo not export by default
-ftemplate-backtrace-limit=0the whole instantiation trace
-ftime-report / -ftime-tracewhere the build went
-fconcepts-diagnostics-depth=Nwhy a concept was not satisfied
-E -S -cstop after preprocess / compile / assemble
-###print the sub-commands and run nothing
-fdump-class-hierarchythe vtable layout
-Rpass=inline / -Rpass-missed=what the optimiser did and did not do

Idioms, and Errors by Symptom

32

Named idioms you will meet in a codebase

RAIIacquire in the constructor, release in the destructor
Rule of Zero / Three / Fivewhich special members to write
Copy-and-swapassignment by value plus swap
PIMPLa pointer to an opaque implementation
CRTPclass D : Base<D>
NVI (non-virtual interface)public non-virtual calls private virtual
Hidden friendoperator defined inside the class
Tag dispatchoverload on an empty tag type
SFINAE / detection idiomremove a candidate on substitution failure
Type erasurea virtual interface behind a value type
EBO / [[no_unique_address]]an empty member costs nothing
Construct on first usea function-local static
Two-step swapusing std::swap; swap(a,b);
Strong typedefenum class, or a one-member struct
std::exchange in a movep_(std::exchange(o.p_, nullptr))
Scope guardrun a lambda in a destructor

Compile and link errors, by what they actually mean

undefined reference to `vtable for X’a virtual was declared and never defined
undefined reference to `X::member’a static data member was declared but not defined
undefined reference, but the function is right therea mangling mismatch
error: no matching function for callno candidate was viable
error: call of overloaded f(…) is ambiguoustwo candidates rank equally
error: there are no arguments to ‘f’ that depend on a template parametertwo-phase lookup
error: need ‘typename’ before …a dependent name assumed to be a value
error: use of deleted functionyou copied a move-only type
error: invalid initialization of non-const referenceyou bound T& to a temporary
error: incomplete typea forward declaration where a definition is needed
warning: control reaches end of non-void functiona missing return -- undefined behaviour
terminate called after throwing …an exception escaped a noexcept function or a destructor
pure virtual method calleda virtual called from a constructor or destructor
free(): invalid pointer / double freemismatched new/delete, or two owners
multiple definition of …a non-inline definition in a header
error: static assertion faileda constraint you wrote