001 Notes

C++ Interview Notes

C++.1 — sizeof of an empty class

class Empty {};
static_assert(sizeof(Empty) == 1);

The standard mandates sizeof(x) >= 1 for any complete object type. Reason: distinct objects must have distinct addresses. If sizeof(Empty) were 0, an array Empty a[10] would have all 10 elements at the same address, breaking &a[i] != &a[j].

Empty Base Optimization (EBO)

When an empty class is a base, it may take zero bytes:

class Empty {};
class Derived : Empty {
    int x;
};
static_assert(sizeof(Derived) == 4);   // EBO: empty base contributes 0

The compiler is allowed (and standard-mandated since C++ added EBO formally) to allocate no storage for an empty base, because the base’s address can coincide with the derived object’s start, and the base has no data members whose addresses could clash.

[[no_unique_address]] (C++20) — EBO for members

struct Empty {};
struct S {
    [[no_unique_address]] Empty e;
    int x;
};
static_assert(sizeof(S) == 4);   // e takes 0 bytes

Without the attribute, Empty e would still consume at least 1 byte plus padding, making sizeof(S) == 8. The attribute lets you put empty allocators, deleters, hashers, etc., into a struct at zero cost — heavily used by std::tuple, std::function, smart pointers with stateless deleters.

Why size 1 and not 0

You couldn’t have:

Empty a, b;
assert(&a != &b);   // must hold

If both took 0 bytes the compiler would have to insert padding anyway. Standard chose to mandate ≥ 1 directly.

Counter-example — empty class with virtual function

class WithVirtual {
public:
    virtual void f() {}
};
sizeof(WithVirtual);   // 8 (or 4) — vptr added

A “no data members” class with a virtual function is no longer empty for EBO purposes — it has a vptr. Same for classes with virtual bases.

What “empty” actually means for EBO

  • No non-static data members
  • No virtual functions
  • No virtual base classes
  • All base classes are empty

If any condition fails, the class needs storage.

Real-world use

template<typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
    T* p;
    [[no_unique_address]] Deleter d;   // free if Deleter is stateless
};

Stateless deleter takes 0 bytes; sizeof(unique_ptr<int>) == sizeof(int*). Stateful deleter pays for its bytes. Same pattern in allocator-aware containers.


C++.2 — RAII

The idea

Resource Acquisition Is Initialization — Bjarne’s badly-named technique. The actual mechanism is the inverse: destruction is resource release. Better name: “Scope-Bound Resource Management” (SBRM).

A class owns a resource. Construction acquires it; destruction releases it. The compiler guarantees destructors run when the object goes out of scope — whether normally, via return, or via exception unwinding.

class File {
    FILE* f;
public:
    explicit File(const char* path) : f(fopen(path, "r")) {
        if (!f) throw std::runtime_error("open failed");
    }
    ~File() { if (f) fclose(f); }
    File(const File&) = delete;
    File& operator=(const File&) = delete;
};

void process() {
    File log("/var/log/x");
    if (something) throw std::runtime_error("oops");
    // log closed automatically on either path
}

Why it matters — exception safety

Code without RAII has to be defensive:

void process() {
    FILE* f = fopen("/var/log/x", "r");
    try {
        if (something) throw std::runtime_error("oops");
        fclose(f);
    } catch (...) {
        fclose(f);
        throw;
    }
}

Every exit path needs cleanup. Add another resource and the combinatorial explosion gets nasty. RAII collapses it to zero ceremony.

Standard library RAII types

TypeResource
std::unique_ptrheap object
std::shared_ptrshared heap object
std::lock_guard, std::scoped_lock, std::unique_lockmutex
std::fstreamOS file handle
std::thread (with jthread C++20)OS thread
std::async’s futurethread + result slot
std::vector, all containersheap buffer

The Rule of Five

A class owning a resource needs to define (or =delete / =default):

class Resource {
public:
    Resource(...);                 // ctor
    ~Resource();                   // dtor
    Resource(const Resource&);     // copy ctor
    Resource& operator=(const Resource&);   // copy assign
    Resource(Resource&&) noexcept; // move ctor
    Resource& operator=(Resource&&) noexcept;  // move assign
};

If your class is a pure RAII wrapper around a single resource, Rule of Zero is better: build it out of standard RAII types and let the compiler generate everything.

class Logger {
    std::unique_ptr<File> f;
    std::vector<std::string> buffer;
};   // no special members needed; correct by construction

Initialization order

Members initialize in declaration order, regardless of init-list order. Destruction is reverse order. Bases before members. This matters when one member’s construction depends on another:

class Conn {
    SocketHandle sock;
    Encryptor enc;        // depends on sock
public:
    Conn(const std::string& host)
        : sock(host)
        , enc(sock) {}    // sock guaranteed constructed first
};

If you swap declarations, you read an uninitialized sock. Compilers warn (-Wreorder) but the bug is silent without warnings.

Two-phase init breaks RAII

class Bad {
    FILE* f = nullptr;
public:
    Bad() {}
    void open(const char* path) { f = fopen(path, "r"); }
    ~Bad() { if (f) fclose(f); }
};

Bad b;            // object exists in invalid state
if (cond) b.open("x");
// caller has to remember to call open

Use the constructor. If construction can fail, throw. Don’t introduce “zombie” states.

Move + RAII

Move-only types are RAII done right:

class File {
    FILE* f;
public:
    File(File&& o) noexcept : f(o.f) { o.f = nullptr; }
    File& operator=(File&& o) noexcept {
        if (this != &o) { if (f) fclose(f); f = o.f; o.f = nullptr; }
        return *this;
    }
    ~File() { if (f) fclose(f); }
};

The moved-from object is left in a “destructible” state (its destructor must run cleanly).

Counter-example — bare new

void leak() {
    auto* p = new Widget;   // no RAII; exception in setup leaks
    setup(p);
    use(p);
    delete p;
}

Three problems: leak on exception, easy to forget delete, unclear ownership. Use std::unique_ptr<Widget> or stack allocation.


C++.3 — Lambda

What a lambda is

A lambda expression is sugar for an anonymous function-object class. The compiler generates a unique class with operator(), instantiates it, and gives you that instance.

auto add = [](int a, int b) { return a + b; };

is roughly:

struct __lambda_xyz {
    auto operator()(int a, int b) const { return a + b; }
};
__lambda_xyz add{};

Each lambda has a unique type, even with identical text:

auto f1 = []{};
auto f2 = []{};
static_assert(!std::is_same_v<decltype(f1), decltype(f2)>);

Captures

int x = 0, y = 0;
auto by_value = [x]{ return x; };               // copies x
auto by_ref   = [&y]{ y++; };                   // references y
auto by_all_val = [=]{ return x + y; };         // copies everything used
auto by_all_ref = [&]{ x++; y++; };             // references everything used
auto init_cap = [n = std::move(big)]{ return n.size(); };   // C++14 init capture
auto this_ptr = [this]{ return member; };       // captures this pointer
auto this_obj = [*this]{ return member; };      // C++17 captures *this by value

The synthesized class has each captured variable as a member:

struct __lambda {
    int x;            // copy
    int& y;           // reference
    auto operator()() const { return x + y; }
};

Mutable

By default operator() is const, so by_value captures are read-only:

auto f = [n = 0]() mutable { return ++n; };    // generates non-const operator()
f(); f(); f();    // returns 1, 2, 3 — state persists per-lambda instance

Each call modifies the captured n (which lives in the lambda object).

Generic lambdas (C++14)

auto f = [](auto x, auto y) { return x + y; };

Equivalent to a template member function:

struct __lambda {
    template<class A, class B>
    auto operator()(A x, B y) const { return x + y; }
};

C++20 added explicit template params:

auto f = []<class T>(T x) { return T{x + x}; };

Conversion to function pointer

A non-capturing lambda converts to a plain function pointer:

auto f = [](int x) { return x + 1; };
int (*fp)(int) = f;     // OK

Capturing lambdas cannot — they have state, function pointers don’t.

Storage and performance

A capture-by-value lambda’s data lives in the lambda object. The lambda object is typically stored:

  • As a local variable (stack)
  • Inside a std::function (heap or SBO)
  • Passed by value to an algorithm (compiler inlines)

Compiler inlines lambdas as aggressively as any other small function — they’re not “slow” because they’re lambdas. They’re fast when passed by value to a template:

std::sort(v.begin(), v.end(), [](int a, int b){ return a > b; });

Compared to std::function, which is type-erased and may heap-allocate the lambda + indirect call.

Capturing this gotcha

struct S {
    int n = 5;
    auto get_lambda() { return [this]{ return n; }; }
};

auto f = S{}.get_lambda();   // temporary S destroyed
f();                          // UB — captured this dangling

[*this] (C++17) copies the object instead:

auto get_lambda() { return [*this]{ return n; }; }    // safe even after S dies

When not to use lambdas

  • Public API surfaces — give them names so callers and tools can refer to them.
  • Recursion — lambdas can’t refer to themselves naturally. C++23 added explicit this auto:
    auto fact = [](this auto self, int n) -> int {
        return n <= 1 ? 1 : n * self(n - 1);
    };
    Pre-C++23 needs std::function (slow) or Y-combinator (clever, ugly).

Counter-example: capturing by reference past lifetime

auto make() {
    int local = 5;
    return [&local]{ return local; };    // dangles after make() returns
}

Always think about lifetime of captured references.


C++.4 — Casting

Five named casts in C++; each enforces a different contract.

static_cast<T>(expr)

Compile-time conversion using the type system’s rules. Allowed:

  • Numeric conversions (int → double, double → int)
  • Pointer up/down within a class hierarchy (downcast not checked at runtime — UB if wrong)
  • void* to typed pointer
  • Explicit conversions defined by the user (explicit operator T)
  • enum to underlying integer and back
double d = static_cast<double>(42);
Base* b = ...;
Derived* d = static_cast<Derived*>(b);    // UB if b is not actually a Derived

Use for: “I know this conversion makes sense, do it.” Catches obviously wrong casts at compile time.

dynamic_cast<T>(expr)

Runtime polymorphic-type check. Requires the source type to have at least one virtual function (so the vptr exists). Returns:

  • The cast pointer if successful
  • nullptr if the source doesn’t actually point to a T (for pointer target)
  • Throws std::bad_cast (for reference target)
Base* b = get_object();
if (auto* d = dynamic_cast<Derived*>(b)) {
    d->derived_only_method();
}

Costs: walks the RTTI graph. Tens of nanoseconds. Compilers can be slow about it.

When to use: when you genuinely need to ask “is this object really a Derived?” Usually a sign your design needs revisiting (use virtual dispatch instead) — but valid for plugin systems, deserialization, sometimes visitors.

const_cast<T>(expr)

Adds or removes const/volatile. Doesn’t change the underlying object’s bits.

const int x = 5;
int& y = const_cast<int&>(x);
y = 10;                          // UB — x is actually const

Modifying an object originally declared const is UB. Useful only for:

  • Calling a legacy C API that takes non-const but doesn’t actually modify.
  • Forwarding to a const-correct overload from a non-const member.

reinterpret_cast<T>(expr)

Bit-pattern reinterpretation. No conversion, no type-system check. Usually:

  • Pointer to integer and back (e.g., for taggable pointers)
  • Pointer to pointer of unrelated type
auto i = reinterpret_cast<std::uintptr_t>(ptr);

Strict aliasing rule: dereferencing a reinterpret_casted pointer typically violates aliasing rules (UB) unless the destination type is char, unsigned char, or std::byte. For type-punning, use std::bit_cast (C++20) or memcpy.

std::bit_cast<T>(src) (C++20)

Type-pun safely:

float f = 1.5f;
std::uint32_t bits = std::bit_cast<std::uint32_t>(f);

Requires sizeof(From) == sizeof(To) and both trivially-copyable. No UB.

C-style cast (T)expr

Tries, in order: const_cast, static_cast, static_cast + const_cast, reinterpret_cast, reinterpret_cast + const_cast. Whichever succeeds is used.

Avoid. You can’t tell from reading the code which kind of cast happened, and a refactor can silently change it from a static_cast to a reinterpret_cast.

Functional-style T(expr)

Same as C-style cast for built-in types. Calls the constructor for class types. Often confused with construction.

int(3.5);              // C-style cast — same as (int)3.5
Widget(args);          // construction — same as Widget{args}

Decision tree

NeedCast
Numeric conversionstatic_cast
Up the hierarchy (Derived→Base)implicit, or static_cast for clarity
Down the hierarchy, sure it’s rightstatic_cast
Down the hierarchy, want runtime checkdynamic_cast
Remove constness from APIconst_cast (almost always wrong)
Type-punning floats/intsstd::bit_cast
Tagged pointers, address arithmeticreinterpret_cast
Anything elsereconsider design

Counter-example — undefined reinterpret_cast deref

int n = 0x3F800000;
float f = *reinterpret_cast<float*>(&n);    // strict aliasing UB

int* is not allowed to alias a float. Use:

float f = std::bit_cast<float>(n);          // C++20, no UB
// or
float f; std::memcpy(&f, &n, sizeof f);     // pre-C++20 idiom

C++.5 — Struct vs class

The technical difference

Exactly one:

  • class: default member access and inheritance is private.
  • struct: default member access and inheritance is public.
class C { int x; };       // x is private
struct S { int x; };      // x is public
class D : Base {};         // private inheritance
struct E : Base {};        // public inheritance

That’s it. Same memory layout, same alignment, same calling conventions, same ABI. You can forward declare with either keyword (and many compilers are lenient about the mismatch, though MSVC isn’t).

The convention

Most codebases follow:

UsePattern
structAggregate / data-bag with no invariants (struct Point { int x, y; };)
classType with private state and public interface enforcing invariants

This convention isn’t enforced by the compiler. Examples:

  • std::pair, std::tuplestruct (data bag)
  • std::string, std::vectorclass (invariants like size ≤ capacity)
  • POD types passed across C boundaries — struct

Aggregate types

A struct/class is an aggregate if:

  • No user-declared constructors
  • No private/protected non-static data members
  • No virtual functions
  • No virtual or non-public bases (relaxed across versions)

Aggregates support brace initialization with member-by-member:

struct Point { int x, y; };
Point p = {1, 2};           // aggregate init
Point q{3, 4};              // since C++11
Point r{.x = 1, .y = 2};    // designated init (C++20)

Making a member private, adding a virtual, or writing a ctor disqualifies the aggregate property.

Aggregate with default members (C++14)

struct Config {
    int port = 8080;
    std::string host = "localhost";
};
Config c{};        // both fields take defaults
Config d{9090};    // port=9090, host=default

Still an aggregate. Defaults don’t count as user-declared ctor.

POD vs trivial vs aggregate

C++11 split “POD” into multiple concepts; C++20 deprecated is_pod:

  • Trivially copyable: memcpy-safe.
  • Trivial: trivially copyable + trivial default ctor.
  • Standard-layout: compatible with C struct layout (no virtuals, single access level for non-static members, etc.).
  • Aggregate: brace-initializable per above.

A C-struct-like type is “trivially copyable + standard layout.”

static_assert(std::is_trivially_copyable_v<Point>);
static_assert(std::is_standard_layout_v<Point>);

When to flip the convention

Some codebases (LLVM, Google C++ style) use struct for any class with only public members regardless of behavior; others use class for any user-defined type. Either is fine; pick one and apply consistently.

Counter-example

struct S {
public:
    void foo();
private:
    int n;
};

Perfectly legal. The only difference is which access section the first member sits in by default. The above struct could just as easily be a class with public: written before void foo();.


C++.6 — new vs normal creation

Stack creation

Widget w(args);
  • Storage from the function’s stack frame.
  • Constructor called immediately.
  • Lifetime ends at the end of the enclosing scope.
  • Destructor runs automatically on scope exit (including exceptions).
  • No heap allocator involved. Effectively free.

new T(args)

Three things happen:

  1. operator new(sizeof(T)) — raw allocation, returns void*.
  2. Placement-new constructs T(args) in that storage.
  3. The expression yields a T*.

If the allocation throws, no ctor runs. If the ctor throws, the allocation is released automatically.

Widget* p = new Widget(args);
delete p;             // calls ~Widget(), then operator delete

Symmetric delete:

  1. ~Widget() runs.
  2. operator delete(p) releases storage.

Allocation hooks

operator new / operator delete are replaceable:

void* operator new(std::size_t n) {
    auto p = my_alloc(n);
    if (!p) throw std::bad_alloc{};
    return p;
}
void operator delete(void* p) noexcept { my_free(p); }

Per-class overrides:

class Pool {
    static void* operator new(std::size_t);
    static void  operator delete(void*);
};

This is how engines and games override allocation policy without changing application code.

Placement new — construct in pre-existing storage

alignas(Widget) std::byte buf[sizeof(Widget)];
auto* p = new (buf) Widget(args);
p->~Widget();          // manual destruction; do NOT call delete!

Used by:

  • std::vector to construct in pre-allocated capacity beyond size
  • std::optional, std::variant
  • Object pools and arena allocators
  • Custom serialization frameworks

Nothrow new

Widget* p = new (std::nothrow) Widget(args);
if (!p) { /* allocation failed */ }

operator new(nothrow_t) returns null instead of throwing. Embedded code uses this when exceptions are disabled.

new[] / delete[]

Array form. Must match: new[] paired with delete[], new paired with delete. Mismatch is UB.

auto* p = new Widget[10];
delete[] p;                  // runs 10 destructors then operator delete[]

Array new often stores the count somewhere (a cookie before the returned pointer) so delete[] knows how many destructors to run. Avoid array new in favor of std::vector or std::array.

Why prefer stack and smart pointers

  • Stack is automatic — no possibility of leak.
  • std::unique_ptr<T> and std::make_unique<T>(args) wrap new + RAII.
  • std::make_shared<T>(args) allocates control block + object together (one allocation).
  • Raw new/delete should be rare in modern code; mostly for implementing the smart pointers themselves.

Counter-example — bare new

void f() {
    Widget* p = new Widget;
    other_that_throws(p);    // leak if it throws — p not destroyed
    delete p;
}

Use auto p = std::make_unique<Widget>(); — leak-proof.


C++.7 — new T vs new T() vs new T{}

The three forms

FormInitialization kind
new Tdefault-initialization
new T()value-initialization
new T{}value-initialization (C++11+, list-init form)
new T(arg)direct-initialization with arg
new T{arg}list-initialization (incl. aggregate init) with arg

Default vs value initialization for built-ins

int* a = new int;        // value uninitialized — garbage
int* b = new int();      // zero — value-initialized
int* c = new int{};      // zero — value-initialized

For built-in types, the parentheses or braces matter. Without them you get a garbage int.

For class types

struct S {
    int x;
    std::string name;
};

auto* p1 = new S;      // default init — x undefined, name default-constructed
auto* p2 = new S();    // value init — x = 0, name default-constructed
auto* p3 = new S{};    // value init — same as ()

For aggregates, value init recursively zero-initializes any built-in members. Default init leaves them indeterminate.

Aggregate brace init

struct Point { int x, y; };
auto* p = new Point{1, 2};   // x=1, y=2 — aggregate init

Narrowing rules

Brace-init forbids narrowing conversions:

int n = 1000;
new char(n);           // OK — narrows silently
new char{n};           // error — narrowing from int to char

This is one reason to prefer brace-init: catches data-loss bugs at compile time.

Most-vexing parse

Widget w();      // declares a function w() returning Widget — NOT an object
Widget w{};      // creates a Widget, no ambiguity

Inside class member initializers, Widget w() is even worse: it’s a function declaration. Always use {} for default-constructed members.

std::vector example

std::vector<int> a(10);       // ten ints, value-init to 0
std::vector<int> b(10, 5);    // ten ints, all 5
std::vector<int> c{10};       // one int with value 10
std::vector<int> d{10, 5};    // two ints: 10 and 5

Brace init with initializer_list<T> constructor wins when the elements are convertible. Surprising for vector users — use () when you mean “size + value.”

Aggregate with no ctor

struct Point { int x, y; };
Point a;          // x, y undefined
Point b{};        // x=0, y=0
Point c{1, 2};    // x=1, y=2
Point d{.x=1};    // C++20 designated, y=0

Performance

Identical machine code in most cases. The difference is what’s in the object afterward. Value-initialization is a slight cost (zeros memory) but pales next to using uninitialized memory and getting bitten by UB.

Counter-example — surprise zero

struct Big { int a, b, c, d, e; };
auto p = new Big();    // zeros all 5 fields — possibly wasted if you set them all anyway

For performance-critical code that immediately overwrites everything, new Big (default init) skips the zeroing. For everything else, prefer new Big{} or make_unique<Big>() — the cost of zeroing is negligible and the safety win is large.


C++.8 — Destruction vs deallocation

Two distinct operations that delete combines.

Destruction

Calling ~T() — runs the destructor. Frees any resources the object held (closes files, frees heap-owned buffers, etc.). Does not release the storage occupied by the object itself.

Deallocation

Calling operator delete(p) — releases the raw memory back to the allocator. Does not call any destructor.

delete p does both

delete p;
// equivalent to:
p->~T();
operator delete(p);

Placement new mandates manual destruction

alignas(Widget) std::byte buf[sizeof(Widget)];
auto* p = new (buf) Widget;

p->~Widget();        // destruction only — buf storage is still valid
                     // do NOT call delete — operator delete would try to free buf

This separation is what enables:

  • std::vector reusing capacity (destroy elements, keep buffer)
  • std::optional and std::variant (storage exists in the struct; constructed and destroyed on demand)
  • Object pools

Storing then re-constructing

T* p = new T(args);
p->~T();             // destroy
new (p) T(other_args);  // re-construct in same storage
// strict aliasing nuance — see C++.13 std::launder

Reusing storage with a different effective type requires std::launder to avoid the optimizer assuming the old object still lives there.

std::destroy_at (C++17)

Generic destruction:

std::destroy_at(p);     // same as p->~T(), but works in templates

std::construct_at (C++20) wraps placement new for use in constant expressions.

Allocators separate the concerns

template<class Alloc>
class my_vector {
    T* data;
    Alloc alloc;

    void destroy(T* p) {
        std::allocator_traits<Alloc>::destroy(alloc, p);   // calls ~T()
    }
    void deallocate(T* p, std::size_t n) {
        std::allocator_traits<Alloc>::deallocate(alloc, p, n); // frees storage
    }
};

destroy runs the destructor; deallocate frees the storage. Containers use these separately to support partial states (e.g., destruct the trailing element on pop_back without shrinking capacity).

Counter-example — destroy without dealloc + leak

T* p = new T;
p->~T();         // memory leak — operator delete never called

You destroyed the object, but the storage is still owned by you and won’t be reclaimed until process exit. Don’t ever call destructor on a new-allocated object directly; use delete.


C++.9 — The diamond problem

Setup

       A
      / \
     B   C
      \ /
       D
struct A { int x; };
struct B : A {};
struct C : A {};
struct D : B, C {};

D contains two A subobjects — one via B, one via C. Accessing D::x is ambiguous:

D d;
d.x = 5;           // error: ambiguous — which A's x?
d.B::x = 5;        // OK — explicit
d.C::x = 5;        // OK — explicit but different field

Memory layout:

+----------+
| B::A::x  |   <-- B's A subobject
+----------+
| (B data) |
+----------+
| C::A::x  |   <-- C's A subobject
+----------+
| (C data) |
+----------+
| (D data) |
+----------+

Two separate As living in the same D. Same issue applies to functions: d.A_method() would be ambiguous unless qualified.

Fix: virtual inheritance

struct A { int x; };
struct B : virtual A {};
struct C : virtual A {};
struct D : B, C {};

D d;
d.x = 5;           // OK — one shared A subobject

Now D has exactly one A subobject, shared between B and C paths.

Cost of virtual inheritance

Each object of a class with virtual bases stores additional pointer(s) (“virtual base table” — vbtbl pointer) to locate the virtual base at runtime, because the offset depends on the most-derived type.

+-----------+
| B vbptr   |   -> points to vtable describing offset to A
| (B data)  |
+-----------+
| C vbptr   |   -> ditto
| (C data)  |
+-----------+
| (D data)  |
+-----------+
| A subobj  |   <-- the single A
+-----------+

Accessing A members from B& or C& requires loading the vbtbl pointer, adding the offset — one extra memory access per dispatch into the virtual base’s data.

Who initializes the virtual base?

The most-derived class. B’s and C’s constructors do not run A’s ctor when they’re a subobject of D — only D initializes A.

struct A { A(int); };
struct B : virtual A { B() : A(1) {} };
struct C : virtual A { C() : A(2) {} };
struct D : B, C {
    D() : A(3), B(), C() {}   // D picks A(3); B and C ignore their A inits
};

Surprising rule but necessary — only one A, so only one initializer wins.

Should you use it?

Mostly: no. Diamond inheritance is a warning sign. Composition and interfaces (pure virtual classes) usually serve better.

When legitimate:

  • iostreams: basic_istream and basic_ostream derive from basic_ios via virtual inheritance; basic_iostream resolves the diamond.
  • Plugin systems with multiple interfaces sharing a common ancestor.

Counter-example — non-virtual is sometimes desired

If B and C truly need independent A state, non-virtual diamond is correct:

struct Counter { int n; };
struct ReadCounter : Counter {};
struct WriteCounter : Counter {};
struct RWCounter : ReadCounter, WriteCounter {};   // two separate counts — intentional

Decide based on semantics: “is the state shared or independent?”


C++.10 — Virtual inheritance

(Largely covered in C++.9; here’s the deeper mechanics.)

What virtual on a base means

class Derived : virtual Base { ... };

Means: “if multiple inheritance chains lead through me to Base, share a single Base subobject across all of them.” The base’s location relative to this depends on the most-derived type.

Memory layout typical (Itanium ABI)

For:

struct A { int x; virtual ~A() = default; };
struct B : virtual A { int b; };
struct C : virtual A { int c; };
struct D : B, C { int d; };

Layout of D:

[B subobject]      <-- D* points here (for B)
  vbptr  -> table { offset to A }
  b
[C subobject]      <-- D* + sizeof(B-stuff) points here
  vbptr  -> table { offset to A }
  c
[D-specific data]
  d
[A subobject]      <-- last (or at start, depending on ABI)
  vptr   -> A's vtable
  x

Each B and C subobject holds a pointer to a virtual-base offset table. When you do pB->x through a B*, the runtime reads vbptr → table → offset → adds to base pointer → adjusts to find A. One indirection more than normal access.

Conversion costs

D d;
B* pb = &d;       // ok, no cost
A* pa = pb;       // needs vbptr lookup to get the actual A address

Casting up to a virtual base is not free — it consults the vbptr at runtime. Casting back down (dynamic_cast<D*>(pa)) is also more expensive than a non-virtual downcast.

Constructor order

Bases are constructed in inheritance order, but virtual bases come first, before any non-virtual bases. So:

struct A {};
struct B : virtual A { B() { /* A already constructed */ } };
struct C { C(B&) {} };
struct D : C, virtual A {
    D() : C(b), b() {}     // A constructed first (virtual), then C, then b, then D
    B b;
};

The “virtual base first” rule means if B accesses A’s members in its constructor, that works correctly even in a diamond.

Use cases

  • iostreams (the canonical example): basic_iostream : basic_istream, basic_ostream, both inherit basic_ios virtually.
  • Interface classes with optional shared state.

Often a smell

If you find yourself drawing a diamond, consider:

  • Inheriting interface only (pure virtual class with no data) — no diamond problem since there’s no data to duplicate, although compilers still flag ambiguity.
  • Composition with delegation.
  • CRTP or other static polymorphism (next section).

C++.11 — Inheritance via TMP (template metaprogramming)

Why static polymorphism

Virtual functions have runtime cost (indirect call) and prevent inlining. For performance-critical hierarchies, static polymorphism via templates eliminates the cost while keeping the abstraction.

CRTP — Curiously Recurring Template Pattern

template<class Derived>
class Shape {
public:
    double area() const {
        return static_cast<const Derived*>(this)->area_impl();
    }
};

class Circle : public Shape<Circle> {
    double r;
public:
    Circle(double r) : r(r) {}
    double area_impl() const { return 3.14159 * r * r; }
};

class Square : public Shape<Square> {
    double s;
public:
    Square(double s) : s(s) {}
    double area_impl() const { return s * s; }
};

template<class T>
double total_area(const Shape<T>& s) { return s.area(); }

Shape<Circle>::area calls Circle::area_impl directly. No vtable, fully inlinable. Each derived class has its own Shape<Derived> base — they’re separate types — so you can’t put them in one container.

When CRTP wins

  • Hot loops with one concrete type at a time
  • Static dispatch in generic algorithms
  • Avoiding the vptr overhead in small objects

When CRTP loses

  • Heterogeneous containers: you can’t put Shape<Circle> and Shape<Square> in one std::vector — different types. Need std::variant<Circle, Square> or runtime polymorphism for that.
  • ABI stability: every derived class instantiates a new template, fattening the binary.

Mixins (CRTP for behavior injection)

template<class Derived>
struct Comparable {
    friend bool operator!=(const Derived& a, const Derived& b) { return !(a == b); }
    friend bool operator<=(const Derived& a, const Derived& b) { return !(b < a); }
    friend bool operator>(const Derived& a, const Derived& b) { return b < a; }
    friend bool operator>=(const Derived& a, const Derived& b) { return !(a < b); }
};

class Point : public Comparable<Point> {
    int x, y;
public:
    friend bool operator==(const Point& a, const Point& b) { return a.x == b.x && a.y == b.y; }
    friend bool operator<(const Point& a, const Point& b) {
        return a.x < b.x || (a.x == b.x && a.y < b.y);
    }
};

You write == and <; the mixin gives you everything else. C++20’s <=> (spaceship) makes this less necessary but mixins still shine for compose-multiple-behaviors.

Tag dispatch

struct fast_path_tag {};
struct slow_path_tag {};

template<class T> struct tag { using type = std::conditional_t<small<T>, fast_path_tag, slow_path_tag>; };

template<class T>
void f(T x, fast_path_tag) { /* SIMD version */ }

template<class T>
void f(T x, slow_path_tag) { /* generic version */ }

template<class T>
void f(T x) { f(x, typename tag<T>::type{}); }

Compile-time selects an overload. Same effect as virtual dispatch but resolved at compile time.

Concepts (C++20) — modern type constraints

template<class T>
concept Shape = requires(T s) {
    { s.area() } -> std::convertible_to<double>;
};

double total(Shape auto const& s) { return s.area(); }

Replaces CRTP “interface” use case cleanly. No inheritance, no extra type, just a constraint.

Counter-example

Don’t use TMP/CRTP if you genuinely need heterogeneous runtime polymorphism. The fact that you might dispatch to different derived classes is exactly what virtual functions are for — paying 5 ns extra is fine.


C++.12 — Aliasing

What aliasing means

Two pointers alias when they refer to overlapping storage. The compiler’s view of aliasing constrains what optimizations it can apply.

Strict aliasing rule

A glvalue may access stored data only through one of:

  • The dynamic type of the object
  • A signed/unsigned variant of that type
  • A char, unsigned char, or std::byte (after C++17)
  • A type covered by cv-qualification of the above

Equivalently: accessing an object through a pointer whose type is unrelated is undefined behavior. The compiler can therefore assume two pointers of unrelated type do not alias — enabling reordering and caching.

void f(int* a, float* b) {
    *a = 1;
    *b = 1.0f;
    *a = 2;        // compiler may reorder, *b can't alias *a (UB if it did)
}

Typical traps

int n = 42;
float f = *reinterpret_cast<float*>(&n);   // UB

Even if the bit pattern is what you want, the read through a float* of memory whose dynamic type is int violates strict aliasing.

Legitimate type-punning

  1. memcpy of byte buffers — always defined:

    float f;
    std::memcpy(&f, &n, sizeof f);    // OK
  2. std::bit_cast (C++20):

    float f = std::bit_cast<float>(n);   // OK
  3. char* aliasing is exempt from strict aliasing:

    void hex_dump(const void* p, size_t n) {
        auto* b = reinterpret_cast<const unsigned char*>(p);   // OK
        for (size_t i = 0; i < n; i++) printf("%02x", b[i]);
    }
  4. Common initial sequence for unions of struct types (limited):

    union U {
        struct { int tag; double d; } a;
        struct { int tag; std::string s; } b;
    };
    U u; u.a.tag = 1;
    int t = u.b.tag;     // OK — first common subobject

Union and type-punning — C++ subtleties

union U { float f; int i; };
U u; u.f = 1.5f;
int n = u.i;     // UB in C++ — only the active member may be read
                 // (C is more permissive)

This is a place where C and C++ differ. In C, reading the non-active member of a union for type-punning is permitted (with caveats). In C++, it’s UB by the letter of the standard — but most compilers do the obvious thing. Use bit_cast or memcpy for portability.

restrict in C++

C++ has no standard restrict. GCC and Clang provide __restrict__ with the same semantics as C restrict. Microsoft has __restrict. Often used in math libraries.

Counter-example — aliasing prevents vectorization

void add(int* a, int* b, int* c, size_t n) {
    for (size_t i = 0; i < n; ++i) c[i] = a[i] + b[i];
}

Compiler must assume a, b, c may alias. So it emits a runtime check version-splitting into “aliased” and “non-aliased” paths, or just scalar code. Mark __restrict__ or write a non-aliasing wrapper to get clean vectorization.


C++.13 — Casting vs std::launder

The problem std::launder solves

When you use placement-new to construct an object of a different type in the storage of a previous object, the compiler may not know the type has changed. Optimizations like alias analysis can use the old type assumption.

struct A { const int n; };
A a{1};
new (&a) A{2};        // construct a new A with n=2 in same storage
int x = a.n;          // UB — see below

a.n is const int = 1. The compiler may cache it as 1 and skip the load. Even though placement-new just made a new A with n = 2, the reference a may still refer to the old object in the compiler’s eyes.

std::launder (C++17)

int x = std::launder(&a)->n;     // 2 — safe

std::launder “launders” the pointer: tells the compiler “treat this pointer as freshly obtained; the object it points to may not be the one you remember.” Optimizer must reload.

When you need launder

Specifically when:

  • The new object has a const or reference member, OR
  • The new object’s type differs from the old one such that pointer-to-old-type wouldn’t be a pointer-interconvertible to pointer-to-new-type.

If the new object has the same type and no const/reference members, no launder needed (the standard’s “pointer-interconvertible” rules cover that case).

Standard library hides it for you

std::optional<T> opt;
opt.emplace(args);    // does placement new internally with launder where needed

Container internals (vector growth, optional reset, variant alternation) use launder so application code rarely sees it.

Cast vs launder — when each applies

SituationTool
Convert between types of an existing objectstatic_cast, dynamic_cast, etc.
Get an int from a float bit-patternstd::bit_cast
Treat raw memory as an object you just placement-new’dstd::launder
Pointer arithmetic, low-level accessreinterpret_cast

Counter-example — wrong tool

void* buf = std::malloc(sizeof(T));
new (buf) T{args};
T* p = reinterpret_cast<T*>(buf);   // works, but doesn't carry "new object" info
                                     // for trivial types, this is fine
                                     // for non-trivial, may need launder
T* q = std::launder(reinterpret_cast<T*>(buf));   // belt and suspenders

For trivially-copyable types stored in a buffer of unrelated type, reinterpret_cast is enough. The launder is needed when const/reference members or strict-aliasing concerns enter the picture.


C++.14 — noexcept and std::vector move behavior

The exception guarantee of vector operations

std::vector provides the strong exception guarantee for many operations: if an operation throws, the vector is left in the state it was in before the operation began.

push_back and reallocation

When size() == capacity(), push_back allocates a new buffer, moves/copies existing elements, constructs the new element, and frees the old buffer.

If during the move-construction of the i-th element, an exception is thrown:

  • Elements 0..i-1 have been moved into the new buffer.
  • Elements i..n-1 are still in the old buffer.
  • The two “halves” of the object exist in different storage.
  • There’s no way back to a consistent state.

To preserve the strong guarantee, vector must use copy instead of move during reallocation — unless it knows move can’t throw.

The rule

vector reallocation uses std::move_if_noexcept(elem):

  • If T’s move constructor is noexcept(true): move.
  • Otherwise: copy.
template<class T>
class Slow {
    T data;
public:
    Slow(const Slow&) = default;
    Slow(Slow&&) /* not noexcept */ {}
};

template<class T>
class Fast {
    T data;
public:
    Fast(const Fast&) = default;
    Fast(Fast&&) noexcept {}
};

A vector<Slow<Big>> will copy during grow → 10× slower than vector<Fast<Big>> if Big has expensive copy.

Verify

static_assert(std::is_nothrow_move_constructible_v<Fast<int>>);
static_assert(!std::is_nothrow_move_constructible_v<Slow<int>>);

Always mark move ctor and move assign noexcept if they really can’t throw — which is true for almost all sane move constructors (just swap pointers).

Why moves are usually noexcept

Move semantics typically:

  1. Steal a pointer/handle from the source.
  2. Reset the source.

No allocation, no exception sources. So noexcept is the right and easy answer. Defaulting your move constructor is noexcept if all subobjects’ moves are.

Counter-example — a move that can throw

class Buffer {
    std::pmr::polymorphic_allocator<std::byte> alloc;
    std::byte* data;
    size_t n;
public:
    Buffer(Buffer&& o) /* not noexcept */
        : alloc(o.alloc), data(alloc.allocate(o.n)), n(o.n)
    {
        std::memcpy(data, o.data, n);
        o.alloc.deallocate(o.data, o.n);
        o.data = nullptr;
    }
};

If the allocators differ between source and destination, you can’t transfer the storage — you have to copy. That copy needs an allocation, which can throw. So this move is rightly non-noexcept. PMR allocators are a real-world case where moves can throw.

Other consequences of noexcept

  • std::move_if_noexcept returns T&& if noexcept-movable, otherwise const T&.
  • Many algorithms (std::variant visit, std::function SBO) have different code paths for noexcept types.
  • terminate() is called if a noexcept function actually throws.

C++.15 — Forwarding reference

What it is

A T&& where T is a template parameter being deduced is a forwarding reference (Scott Meyers called it “universal reference”). It can bind to lvalues or rvalues, preserving the value category.

template<class T>
void f(T&& x);   // forwarding reference

int n = 5;
f(n);   // T = int&, x is int&
f(5);   // T = int,  x is int&&

NOT a forwarding reference:

void f(int&& x);            // plain rvalue reference — only binds rvalues
template<class T>
void f(const T&& x);        // plain rvalue ref to const — not forwarding
template<class T>
void f(std::vector<T>&& x); // not template param T alone — not forwarding

The pattern is specifically T&& where T is being deduced.

Reference collapsing

When references compose (template substitution, typedefs), the rules are:

CombineResult
T& &T&
T& &&T&
T&& &T&
T&& &&T&&

Lvalue wins. This is what lets T&& “absorb” an lvalue argument and remain an lvalue reference.

std::forward<T>(x)

Re-emits x with its original value category:

template<class T>
void wrapper(T&& arg) {
    callee(std::forward<T>(arg));
}

If arg was bound to an rvalue, std::forward<T>(arg) gives an rvalue. If arg was bound to an lvalue, std::forward<T>(arg) gives an lvalue. The actual implementation:

template<class T>
T&& forward(std::remove_reference_t<T>& x) noexcept {
    return static_cast<T&&>(x);
}

Reference collapsing on T&&:

  • If T = X (passed rvalue), result is X&&.
  • If T = X& (passed lvalue), result is X&.

Why std::move is different

std::move(x) unconditionally casts to rvalue:

template<class T>
std::remove_reference_t<T>&& move(T&& x) noexcept {
    return static_cast<std::remove_reference_t<T>&&>(x);
}

You use move when you want to give up x. You use forward in templates when you want to preserve whatever the caller passed.

Perfect forwarding factory

template<class T, class... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

Every argument keeps its value category, so lvalues are copied/copy-constructed and rvalues are moved into T.

Common bug — using move instead of forward

template<class T>
void bad(T&& x) {
    callee(std::move(x));    // x is now always rvalue inside callee
}

int n = 5;
bad(n);     // n is now moved-from (in unspecified-but-valid state)
            // surprising for caller — they didn't authorize a move

Forwarding refs in generic code mean the caller decides lvalue vs rvalue. Don’t unilaterally move.

Forwarding in lambdas

auto f = [](auto&& x) {
    callee(std::forward<decltype(x)>(x));
};

auto&& parameter is a forwarding reference; decltype(x) gives the correct T for forwarding.

Counter-example — forwarding ref taking a const T&&

template<class T>
void f(const T&& x);    // T deduced; with const, NOT a forwarding ref

int n = 5;
f(n);   // error — no lvalue version

The const makes it a plain rvalue reference to const. Caller can only pass rvalues.


C++.16 — Implement unique_ptr, shared_ptr

unique_ptr — single-owner

Minimum viable:

template<class T, class Deleter = std::default_delete<T>>
class unique_ptr {
    T* p;
    [[no_unique_address]] Deleter d;
public:
    explicit unique_ptr(T* p = nullptr) noexcept : p(p) {}
    ~unique_ptr() { if (p) d(p); }

    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;

    unique_ptr(unique_ptr&& o) noexcept
        : p(o.p), d(std::move(o.d)) { o.p = nullptr; }

    unique_ptr& operator=(unique_ptr&& o) noexcept {
        if (this != &o) {
            if (p) d(p);
            p = o.p; d = std::move(o.d);
            o.p = nullptr;
        }
        return *this;
    }

    T& operator*() const noexcept { return *p; }
    T* operator->() const noexcept { return p; }
    T* get() const noexcept { return p; }

    T* release() noexcept { T* r = p; p = nullptr; return r; }
    void reset(T* np = nullptr) noexcept {
        T* old = p; p = np;
        if (old) d(old);
    }
    explicit operator bool() const noexcept { return p != nullptr; }
};

Size = sizeof(T*) if Deleter is stateless (EBO via [[no_unique_address]]); larger if Deleter has state.

make_unique

template<class T, class... Args>
unique_ptr<T> make_unique(Args&&... args) {
    return unique_ptr<T>(new T(std::forward<Args>(args)...));
}

Why use make_unique over unique_ptr<T>(new T(...)):

  1. Symmetry with make_shared.
  2. Pre-C++17 had an evaluation-order bug:
    f(unique_ptr<A>(new A), other_throws());
    Compiler could reorder: new A, other_throws() throws, leak. C++17 fixed this. make_unique was a workaround.

shared_ptr — shared ownership with reference count

Control block layout:

shared_ptr<T>
  +-- ptr -> T*       (or aliased ptr)
  +-- ctrl -> control_block
                          +-- strong_count (atomic)
                          +-- weak_count   (atomic)
                          +-- deleter
                          +-- allocator
                          +-- optional in-place T storage (if make_shared)

Minimum viable:

struct control_block_base {
    std::atomic<long> strong{1};
    std::atomic<long> weak{1};       // owners + (strong > 0 ? 1 : 0)
    virtual void destroy() noexcept = 0;
    virtual void deallocate() noexcept = 0;
    virtual ~control_block_base() = default;
};

template<class T, class Deleter, class Alloc>
struct control_block : control_block_base {
    T* p;
    Deleter d;
    Alloc a;
    control_block(T* p, Deleter d, Alloc a) : p(p), d(std::move(d)), a(std::move(a)) {}
    void destroy() noexcept override { d(p); }
    void deallocate() noexcept override {
        using rebind = typename std::allocator_traits<Alloc>::template rebind_alloc<control_block>;
        rebind ra(a);
        std::allocator_traits<rebind>::destroy(ra, this);
        std::allocator_traits<rebind>::deallocate(ra, this, 1);
    }
};

template<class T>
class shared_ptr {
    T* p;
    control_block_base* ctrl;

    void acquire() noexcept {
        if (ctrl) ctrl->strong.fetch_add(1, std::memory_order_relaxed);
    }
    void release() noexcept {
        if (ctrl && ctrl->strong.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            ctrl->destroy();
            if (ctrl->weak.fetch_sub(1, std::memory_order_acq_rel) == 1)
                ctrl->deallocate();
        }
    }
public:
    shared_ptr() noexcept : p(nullptr), ctrl(nullptr) {}
    template<class U>
    explicit shared_ptr(U* raw)
        : p(raw),
          ctrl(new control_block<U, std::default_delete<U>, std::allocator<U>>(
              raw, std::default_delete<U>{}, std::allocator<U>{}))
    {}

    shared_ptr(const shared_ptr& o) noexcept : p(o.p), ctrl(o.ctrl) { acquire(); }
    shared_ptr(shared_ptr&& o) noexcept : p(o.p), ctrl(o.ctrl) { o.p = nullptr; o.ctrl = nullptr; }

    shared_ptr& operator=(shared_ptr o) noexcept { swap(o); return *this; }

    ~shared_ptr() { release(); }

    void swap(shared_ptr& o) noexcept { std::swap(p, o.p); std::swap(ctrl, o.ctrl); }

    T& operator*() const noexcept { return *p; }
    T* operator->() const noexcept { return p; }
    T* get() const noexcept { return p; }
    long use_count() const noexcept { return ctrl ? ctrl->strong.load() : 0; }
};

Memory orderings on the count

  • Increment: relaxed is fine. Counting up doesn’t synchronize anything; if you have a pointer you already have visibility on the object.
  • Decrement: must be acq_rel. Acquire to see all writes by the prior owner (so the destructor sees the final state); release to make the decrement visible to whoever destroys.

This is the standard “boost::shared_ptr-style” implementation, found in libstdc++ and libc++.

make_shared vs shared_ptr(new T)

make_shared<T>(args...) allocates one chunk holding both the control block and T:

[control_block_inplace][T data...]

Pros:

  • Single allocation instead of two.
  • Better locality.
  • The “in-place” control block stores T directly; no separate pointer.

Cons:

  • T’s storage is freed only when the last weak_ptr dies — because the storage backing T is shared with the control block. With shared_ptr(new T), the T storage is freed on last shared_ptr; the control block on last weak_ptr.
  • Can’t use custom deleter.
  • Memory footprint may stay live longer if you keep weak_ptrs around.

weak_ptr

template<class T>
class weak_ptr {
    T* p;
    control_block_base* ctrl;
public:
    weak_ptr(const shared_ptr<T>& o) noexcept : p(o.p), ctrl(o.ctrl) {
        if (ctrl) ctrl->weak.fetch_add(1, std::memory_order_relaxed);
    }
    ~weak_ptr() {
        if (ctrl && ctrl->weak.fetch_sub(1, std::memory_order_acq_rel) == 1)
            ctrl->deallocate();
    }

    shared_ptr<T> lock() const noexcept {
        if (!ctrl) return {};
        long s = ctrl->strong.load(std::memory_order_relaxed);
        do {
            if (s == 0) return {};
        } while (!ctrl->strong.compare_exchange_weak(s, s + 1,
                                                     std::memory_order_acq_rel,
                                                     std::memory_order_relaxed));
        shared_ptr<T> r;
        r.p = p; r.ctrl = ctrl;
        return r;
    }
};

lock() does CAS on the strong count — if it’s already 0, the object is dead.

Aliasing constructor

struct Big { int a, b; };
shared_ptr<Big> big = std::make_shared<Big>();
shared_ptr<int> alias(big, &big->a);   // shares ownership of Big, but ptr-> is to .a

When alias’s reference count hits zero, both big’s storage and alias’s view are deallocated together. Useful for sharing ownership of a sub-object.

Atomic shared_ptr operations

std::atomic<std::shared_ptr<T>> (C++20) provides lock-free atomic ops on shared_ptr. Previously requires DCLP-style code or external lock.

Counter-example — circular reference leak

struct Node {
    std::shared_ptr<Node> next;
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a;     // cycle — a and b never freed

Break with std::weak_ptr:

struct Node {
    std::weak_ptr<Node> next;   // back-pointer doesn't own
};

C++.17 — Virtual destructors

Rule

If a class is intended to be deleted polymorphically (delete base_ptr where the actual object is a derived type), its destructor must be virtual.

struct Base { ~Base() {} };          // non-virtual
struct Derived : Base { ~Derived() { /* important cleanup */ } };

Base* p = new Derived;
delete p;                            // UB — only ~Base() called, Derived's cleanup skipped

Make ~Base() virtual and now delete p calls ~Derived() then ~Base().

Why it’s special

The destructor is the only function whose name doesn’t match the class hierarchy (“~Base” vs “~Derived”). The virtual dispatch is for the unqualified destructor call generated by delete.

Pure virtual destructor

struct Interface {
    virtual ~Interface() = 0;        // pure virtual
};
Interface::~Interface() {}            // MUST provide a definition

The definition is required because derived destructors implicitly call the base destructor. Without a body, the link fails.

When to avoid virtual dtor

  • Non-polymorphic types (PODs, value types, types you don’t intend to delete via base pointer).
  • Performance-critical classes where the vptr is unwanted.
  • Marker classes used only via static_cast.

final for polymorphic types

If a class shouldn’t be inherited from, mark it final:

class Impl final : public Interface { ... };

Tells compiler & readers no further override; enables devirtualization in some cases.

Counter-example — virtual dtor without slicing

unique_ptr<Base> correctly destroys derived objects via virtual dtor — but unique_ptr<Base[]> is dangerous: it uses delete[], which doesn’t go through virtual dispatch element-by-element. Never use array-form unique_ptr<Base[]> for polymorphic types.


C++.18 — Destructor throws during stack unwinding

The trap

If an exception is propagating (stack unwinding is happening), and a destructor that runs as part of unwinding also throws, the runtime calls std::terminate(). The program dies.

struct Bad {
    ~Bad() noexcept(false) {
        throw std::runtime_error("dtor!");
    }
};

void f() {
    Bad b;
    throw std::runtime_error("outer");    // unwinding starts
    // ~b() runs during unwind; throws; terminate()
}

Why

Two exceptions can’t be in flight simultaneously. The runtime has no policy that says “ignore one, propagate the other.” Terminating is the only safe choice.

Detecting unwinding

struct Logger {
    ~Logger() {
        if (std::uncaught_exceptions() > 0) {
            // we're being destroyed during exception propagation
        }
    }
};

std::uncaught_exceptions() (C++17) returns the count; lets you behave differently in a destructor depending on whether you’re in exceptional cleanup vs normal exit. Older std::uncaught_exception() (singular) returned bool but had subtle issues.

Best practice

Destructors should be noexcept by default (which they are unless you opt out or your bases/members aren’t). If your destructor calls anything that might throw:

struct Conn {
    ~Conn() noexcept try {
        send_close();
    } catch (...) {
        log("close failed");
        // do not rethrow
    }
};

Swallow the exception in a destructor. If close() failing must be reported, expose an explicit close() method the caller calls before destruction.

Standard library guarantees

All standard library destructors are noexcept. Throw out of a vector::~vector()? terminate.

Counter-example — silently broken legacy code

struct Old {
    Resource* r;
    ~Old() { release(r); /* release can throw */ }
};

vector<Old> v;
// any normal exception in v's code → unwind → ~Old() may throw → terminate

Fix: wrap in noexcept, swallow, log.


C++.19 — Delegating constructor

C++11 feature: one constructor calls another in the same class.

struct S {
    int n;
    std::string name;

    S() : S(0, "anon") {}                       // delegate to (int, string)
    S(int n) : S(n, "anon") {}                  // delegate
    S(const char* name) : S(0, name) {}         // delegate
    S(int n, std::string name) : n(n), name(std::move(name)) {}    // the real one
};

Rule: must appear alone in init list

S() : S(0), n(5) {}     // error — delegating call must be the only entry

You delegate or initialize members; not both.

Order of construction

Delegating ctor runs the delegated ctor first (fully — its body and all member initializations), then the delegating ctor’s body. Members are not re-initialized.

struct S {
    int n;
    S(int n) : n(n) { /* body A */ }
    S() : S(42) { /* body B */ }
};

S s;    // executes A first (n becomes 42), then B

Exception safety

If the delegated ctor throws, the object is considered destroyed for purposes of unwinding — its destructor will be called.

Counter-example — recursion

struct R {
    R() : R(0) {}
    R(int) : R() {}     // infinite recursion at runtime — compiles fine
};

Compiler doesn’t detect the cycle. Stack overflow.


C++.20 — Inheriting constructor

C++11 feature: bring base class constructors into derived scope.

struct Base {
    Base(int);
    Base(double);
    Base(const std::string&);
};

struct Derived : Base {
    using Base::Base;   // inherit all base constructors
};

Derived d1(5);
Derived d2(3.14);
Derived d3("hi");

What’s actually generated

For each base ctor with signature (P...), the compiler synthesizes a derived ctor:

Derived(P... args) : Base(std::forward<P>(args)...) {}

Members of Derived are default-initialized.

Caveats

  1. Default arguments are not inherited; each is a separate ctor signature.

    struct Base { Base(int = 0, int = 0); };

    Derived gets three ctors: (), (int), (int,int).

  2. Member default init only:

    struct Derived : Base {
        int extra;
        using Base::Base;
    };
    Derived d(5);
    // d.extra is default-initialized (uninitialized for int)

    Provide an in-class member initializer:

    int extra = 0;
  3. Implicit ctors of Base (default, copy, move) are NOT inherited — you get the implicit ones for Derived separately.

  4. C++17 fixes: more careful semantics around what’s inherited, especially for templates.

When to use

  • Wrapping a base type to add behavior without re-declaring its API
  • Exception types that mirror parent’s constructors

Counter-example — when it surprises

struct Base { Base(int); };
struct Derived : Base {
    using Base::Base;
    int member;            // uninitialized!
};

Derived d(5);
std::cout << d.member;    // UB

Always initialize members in-class when inheriting ctors.


C++.21 — Default vs value vs zero vs aggregate initialization

The terminology

Five common terms, each with a precise meaning:

  1. Default initializationT x; (or new T;)
  2. Value initializationT x{};, T(), new T()
  3. Zero initialization — sets bits to zero (intermediate step in value-init for built-ins)
  4. Direct initializationT x(args);
  5. Copy initializationT x = expr;
  6. List initializationT x{...};
  7. Aggregate initialization — list-init applied to an aggregate

Default initialization

int x;            // uninitialized (UB to read)
std::string s;   // calls default ctor → ""
struct S { int n; };
S a;              // S has implicit default ctor; n is uninitialized

Built-ins are left uninitialized; class types call their default constructor. If no default ctor exists or is accessible, error.

Value initialization

int x{};                       // 0
std::string s{};               // ""
S a{};                         // n = 0 (aggregate => zero-init members)

auto p = new int();            // 0
auto q = new S();              // n = 0

For built-ins: zero-initialize. For class with user-declared default ctor: call it. For aggregates: zero-init each member, then call any default member initializers.

Zero initialization

Sets the object’s bit representation to zero. Performed:

  • For static and thread-storage variables at startup (before any other init).
  • As the first step in value-init of a non-class type.
  • For members of a union with {} if no member initializer.

Direct initialization

T x(args);
T x{args};
new T(args)
T(args)

Calls the matching ctor. With braces, also checks for initializer_list ctor first.

Copy initialization

T x = expr;
T x = {a, b};
f(expr);          // passing argument
return expr;      // returning value

Conceptually constructs a T from expr, then copies/moves to x. With copy elision (mandatory since C++17 for prvalues), no actual copy happens.

Copy-init disallows explicit constructors:

struct S { explicit S(int); };
S a = 5;          // error — explicit ctor not considered in copy-init
S a(5);           // OK — direct
S a{5};           // OK — direct-list

Aggregate initialization

For aggregates (no user-declared ctor, no private non-static members, no virtual functions):

struct Point { int x, y; };

Point a = {1, 2};        // aggregate, copy-style
Point b{1, 2};            // aggregate, direct-style
Point c = {.x = 1, .y = 2};   // designated (C++20)

Fewer initializers than members: trailing members value-initialized.

Point d{1};       // x=1, y=0
Point e{};        // x=0, y=0

Excess initializers: error.

The big table

SyntaxFor built-inFor class with default ctorFor aggregate
T x;uninitcalls ctorcalls ctor (or uninit members)
T x{};0calls ctorzero-init members
T x();declares function!declares function!declares function!
T()prvalue, value-initprvalue, value-initprvalue, value-init
new Tuninitcalls ctoruninit members
new T()0calls ctorzero-init members
new T{}0calls ctorzero-init members

Counter-example — the most-vexing parse

struct S { S(); };
S x();          // declares a function returning S — NOT an object
S x{};          // creates an S
S x = S();      // creates an S

Always use {} for default construction unless you have a reason to use ().


C++.22 — vtable vs vptr

What they are

  • vtable (virtual function table): per-class array of function pointers, one per virtual method. Lives in .rodata.
  • vptr (virtual pointer): per-object pointer to the appropriate vtable. Lives at the start of the object (typical Itanium ABI).

Layout example

struct Animal {
    virtual void speak() = 0;
    virtual ~Animal() = default;
    int age;
};

struct Dog : Animal {
    void speak() override;
    void fetch();
    int breed_id;
};

In memory:

Animal vtable:                          Dog vtable:
+----------------+                      +-----------------+
| &Animal::~A    |                      | &Dog::~Dog      |
| &Animal::speak | (pure: __cxa_pure)   | &Dog::speak     |
+----------------+                      +-----------------+

Animal object:        Dog object:
+----------+          +----------+
| vptr ----+--------> | vptr ----+--------> Dog vtable
| age      |          | age      |
+----------+          | breed_id |
                      +----------+

When the compiler sees animal_ptr->speak(), it emits:

load vptr from *animal_ptr
load function pointer from vtable[speak_slot]
call function pointer

Three loads (object → vptr → vtable → function), then an indirect call. About 1–2 ns extra over a direct call, but breaks inlining.

Where vptr is set

  • Constructor entry: vptr set to current class’s vtable.
  • Constructor body of Derived: by the time Derived’s ctor body runs, vptr has been changed to Derived’s vtable (Base’s was set earlier).

Consequence: calling a virtual function from a constructor does not dispatch to derived overrides. The vptr is still pointing at the base vtable.

struct Base {
    Base() { speak(); }         // calls Base::speak — even from Derived
    virtual void speak() {}
};
struct Derived : Base {
    void speak() override {}
};

Derived d;                       // prints Base::speak

Same applies in destructor: vptr is reset to base’s during base’s destructor, so virtual calls dispatch to base.

Why dynamic_cast costs more

It walks RTTI (type_info objects, hierarchy data) reachable from the vtable.

Size cost

Per-class: vtable sized by virtual function count (small). Per-object: one pointer (8 bytes on 64-bit) for the vptr.

So a class with one int and a virtual function is 16 bytes (8 vptr + 4 int + 4 padding), not 4.

Multiple inheritance

Each base with virtuals contributes its own vptr. Conversions to bases may adjust this:

struct A { virtual void a(); };
struct B { virtual void b(); };
struct D : A, B { void a() override; void b() override; };

D d;
A* pa = &d;
B* pb = &d;
// pa and pb point at different offsets within d

When pb->b() calls D::b, the compiler generates a thunk that adjusts this back to the D start before calling.

Devirtualization

If the compiler can prove the dynamic type (e.g., Derived d; d.f(); — type known), it elides the vtable indirection. final helps:

class Dog final : public Animal { ... };

Now any Dog* is known to be exactly Dog, enabling direct calls and inlining.

Counter-example — virtual called from ctor “doesn’t work”

Already shown above. Solution: have the caller call the virtual after construction:

auto d = std::make_unique<Derived>();
d->init();    // virtual init called after vptr is properly Derived's

Or use static polymorphism (CRTP).


C++.23 — override keyword

C++11 contextual keyword on a derived virtual function declaration. Tells the compiler “I’m overriding a base virtual function” — and if no matching base virtual exists, error.

struct Base {
    virtual void foo(int) const;
};

struct Derived : Base {
    void foo(int) const override;       // OK
    void foo(int);                       // hides — not override
    void foo(double) const override;     // error — different signature
    void bar() override;                 // error — Base has no bar
};

What it catches

  • Typo: Foo instead of foo.
  • Const mismatch: void foo(int) vs void foo(int) const.
  • Reference qualifier: void foo() & vs void foo().
  • Return type covariance bugs.
  • Removing virtual from a base method silently breaks all overrides — override makes it loud.

final is the bookend

struct A { virtual void f(); };
struct B : A { void f() final; };     // f cannot be overridden in further derived
struct C : B { void f() override; };  // error: B::f is final

final on the class:

class B final : public A { ... };     // B itself can't be inherited from

Both enable devirtualization opportunities (the compiler knows the dispatch endpoint).

Style: always use override

Modern C++ style (Google, LLVM, Core Guidelines) writes override on every override; doesn’t repeat virtual. The compiler infers virtualness from the override relationship.

struct Derived : Base {
    void foo() override;       // ✓
};

versus:

struct Derived : Base {
    virtual void foo();        // works, but is the redundant `virtual` actually overriding?
};

Counter-example — hidden overload

struct Base { virtual void f(int); };
struct Derived : Base {
    void f(double);           // overload, hides Base::f(int)
};

Derived d;
d.f(5);                       // calls f(double) with conversion
                              // NOT Base::f(int)

override would catch this immediately. using Base::f; inside Derived also unhides.


C++.24 — dynamic_cast

What it does

Runtime-checked cast within a polymorphic class hierarchy. Requires the source type to have at least one virtual function (so the vptr exists for RTTI access).

struct Animal { virtual ~Animal() = default; };
struct Dog : Animal { void fetch(); };
struct Cat : Animal { void meow(); };

Animal* a = get_some_animal();
if (Dog* d = dynamic_cast<Dog*>(a)) {
    d->fetch();
}

Variants

FormFailure
dynamic_cast<T*>(p)returns nullptr
dynamic_cast<T&>(ref)throws std::bad_cast

Implementation

Compilers store an RTTI table reachable from each vtable. The cast:

  1. Read vptr from *p (already required by vtable).
  2. Index into RTTI to get the type_info and inheritance graph.
  3. Walk the graph to find a path from dynamic type to T.
  4. Adjust this by accumulated offsets (multiple inheritance).
  5. Return result.

Cost: ~10–50 ns for simple hierarchies, more for deep/multi-inheritance. There’s a typeid instruction too:

if (typeid(*a) == typeid(Dog))
    // a's dynamic type is exactly Dog

typeid is faster (just two pointer compares) but doesn’t handle inheritance — it requires exact type match.

Cross-cast

struct A { virtual ~A(); };
struct B { virtual ~B(); };
struct C : A, B {};

A* pa = new C;
B* pb = dynamic_cast<B*>(pa);   // sideways through C

dynamic_cast can navigate from one base to a sibling base via the common most-derived class. static_cast cannot.

When to use

  • Plugin systems where a registered object may be one of several derived types.
  • Visitor variants (though std::variant is usually better).
  • Diagnostic tools that need to introspect types.

When NOT to use

  • “Smell” indicator: if every method needs dynamic_cast, virtual dispatch is your design tool.
  • Hot paths: 50 ns adds up.

Often dynamic_cast is the wrong tool because the type-switch should be encoded as virtual methods.

Counter-example — types without virtuals

struct Animal {};
struct Dog : Animal {};

Animal* a = new Dog;
Dog* d = dynamic_cast<Dog*>(a);   // error: Animal not polymorphic

You need at least one virtual function (typically a virtual destructor) in the base for dynamic_cast to work.

Alternative — open-method dispatch / std::variant

std::variant<Dog, Cat> v = Dog{};
std::visit([](auto& a) { /* a is Dog or Cat */ }, v);

For closed sets of types, variant + visit is faster, type-safe, and self-documenting. dynamic_cast is for genuinely open hierarchies.


C++.25 — decltype, std::enable_if, std::integral_constant, constexpr

decltype

Gives you the type of an expression at compile time, without evaluating it.

int x = 5;
decltype(x) y = 10;          // int

auto f() -> decltype(some_expr) { ... }   // trailing return type

Subtle distinction:

  • decltype(name) — declared type of the entity (often T).
  • decltype(expr) — type of the expression as an lvalue/xvalue/prvalue category.
    • prvalue → T
    • lvalue → T&
    • xvalue → T&&
int n;
decltype(n) a;       // int — name lookup
decltype((n)) b;     // int& — parenthesized expression is an lvalue

decltype(auto) (C++14) uses decltype rules for return-type deduction instead of auto’s (which strips refs).

std::enable_if (SFINAE)

template<class T,
         std::enable_if_t<std::is_integral_v<T>, int> = 0>
T abs(T v) { return v < 0 ? -v : v; }

If the condition is false, the substitution fails, and this overload silently disappears from the candidate set. Other overloads compete.

Use cases:

  • Disable an overload for certain types.
  • Constrain a template’s range of acceptable types pre-C++20.

C++20 superseded this with requires/concepts:

template<std::integral T>
T abs(T v) { return v < 0 ? -v : v; }

Much cleaner error messages and intent.

std::integral_constant

Wraps a compile-time integer value as a type.

using True = std::integral_constant<bool, true>;
True::value;     // true
True{}();        // true (operator())

Used in tag-dispatch and to lift values into the type system. std::true_type and std::false_type are common aliases.

constexpr

Marks a function/variable as constant-expression-eligible. Inside, only constexpr-safe operations.

constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

constexpr int f = factorial(5);   // computed at compile time
int g = factorial(read());         // computed at runtime, function still constexpr

Newer flavors:

  • consteval (C++20): must be evaluated at compile time. Compile error if not.
  • constinit (C++20): variable must be const-init’d, but not necessarily const.
  • if constexpr (C++17): branch eliminated at compile time, replacing many enable_if uses.
    template<class T>
    auto print(const T& v) {
        if constexpr (std::is_pointer_v<T>) std::cout << *v;
        else std::cout << v;
    }

Combined — type-traits style

template<class T>
struct is_integral_pointer
    : std::integral_constant<bool,
          std::is_pointer_v<T> &&
          std::is_integral_v<std::remove_pointer_t<T>>>
{};

static_assert(is_integral_pointer<int*>::value);

This is the pre-C++20 way to build custom traits.

Counter-example — enable_if on a non-template

class S {
    void f(int);          // overload set
    void f(double);
};

You can’t enable_if between two member functions of the same name — overload resolution sees them simultaneously. Use overload-rank tag dispatch instead.


C++.26 — std::allocator, std::pmr::memory_resource

The classic Allocator interface

Every standard container takes an Allocator template parameter; defaults to std::allocator<T>. The interface allows containers to use custom allocation.

template<class T>
struct my_alloc {
    using value_type = T;
    T* allocate(std::size_t n) { return static_cast<T*>(::operator new(n*sizeof(T))); }
    void deallocate(T* p, std::size_t) { ::operator delete(p); }
    template<class U> bool operator==(const my_alloc<U>&) const noexcept { return true; }
};

std::vector<int, my_alloc<int>> v;

Problems with the classic design

  • Allocator type is part of the container type. vector<int, A> and vector<int, B> are unrelated types — can’t pass to the same function easily.
  • Compile times balloon (every container/allocator combo is its own template instantiation).
  • Mixing allocators in collections of containers is painful.

std::pmr::memory_resource (C++17)

Polymorphic memory resource: runtime-polymorphic base class.

class memory_resource {
public:
    void* allocate(size_t bytes, size_t align = ...) { return do_allocate(bytes, align); }
    void  deallocate(void* p, size_t bytes, size_t align = ...) { do_deallocate(p, bytes, align); }
    bool  is_equal(const memory_resource& other) const noexcept { return do_is_equal(other); }
private:
    virtual void* do_allocate(size_t, size_t) = 0;
    virtual void  do_deallocate(void*, size_t, size_t) = 0;
    virtual bool  do_is_equal(const memory_resource&) const noexcept = 0;
};

Then std::pmr::polymorphic_allocator<T> is a thin allocator that holds a memory_resource* and forwards calls. Different containers with different memory_resources have the same type (std::pmr::vector<int>).

Provided memory resources

std::pmr::new_delete_resource();        // default — calls ::new/::delete
std::pmr::null_memory_resource();       // throws bad_alloc on any allocate
std::pmr::synchronized_pool_resource;   // size-class pool with thread safety
std::pmr::unsynchronized_pool_resource; // size-class pool, single-thread
std::pmr::monotonic_buffer_resource;    // bump allocator, deallocate is no-op

Arena allocator pattern

std::byte buf[1 << 16];
std::pmr::monotonic_buffer_resource arena{buf, sizeof buf, std::pmr::null_memory_resource()};
std::pmr::vector<int> v{&arena};
v.reserve(1000);
// no heap allocation — all from buf

When arena goes out of scope, all the allocations are released en masse. Perfect for request-scoped work, parsers, etc.

Chaining

std::pmr::monotonic_buffer_resource arena{1 << 16};       // upstream = new_delete_resource
std::pmr::unsynchronized_pool_resource pool{&arena};

The pool gets pages from the arena; the arena gets giant chunks from new. When the arena dies, everything is freed.

Performance

  • new_delete_resource: same speed as raw new/delete.
  • monotonic_buffer_resource: O(1) allocate (bump), free is a no-op until reset. Fastest.
  • pool_resource: O(1) allocate within a size class; good for many small same-size allocs.

Counter-example — sharing allocators across move

std::pmr::vector<int> a{&arena1};
std::pmr::vector<int> b{&arena2};
a = std::move(b);

a and b have different allocators. Move-assign cannot transfer storage. PMR moves are not noexcept — vector falls back to copy during grow. This is the case the C++.14 question was about.


C++.27 — emplace_back vs push_back

push_back

Takes an object of T (by value with overloads), then copies or moves it into the container.

v.push_back(Widget{1, 2});      // construct temporary, then move
v.push_back(my_widget);          // copy
v.push_back(std::move(my_widget)); // move

emplace_back

Takes constructor arguments. Forwards them to T’s ctor, constructing in place at the back of the container.

v.emplace_back(1, 2);            // constructs Widget(1, 2) directly in the buffer

When emplace wins

Avoiding the temporary:

struct Big {
    Big(int a, int b);             // expensive ctor
    Big(const Big&);               // expensive copy
    Big(Big&&) = delete;           // not movable
};

std::vector<Big> v;
v.push_back(Big(1, 2));            // error — no move, can't temp
v.emplace_back(1, 2);              // OK — constructed in place

For movable types, the saving is one move construction per call. Often negligible — moves are cheap.

When emplace loses

std::vector<std::string> v;
v.reserve(1);
v.emplace_back("hello");
v.emplace_back(v[0]);     // dangling reference!
                          // emplace constructs from forwarded reference
                          // forwarding ref keeps it as a reference into v[0]
                          // first emplace may reallocate, invalidating v[0]

The forwarding reference can keep references alive past their utility. push_back is “safer” here because the argument is converted to a temporary first.

Surprising cases

std::vector<int> v;
v.emplace_back(10);    // OK — emplaces int(10)

std::vector<std::vector<int>> vv;
vv.emplace_back(10);   // vector(size_t = 10) — emplaces a vector of 10 default ints
vv.push_back({10});    // vector{10} — emplaces a vector with one element = 10

Mismatch between emplace (ctor args) and push_back (an instance) can be confusing.

try_emplace / emplace for map

std::map<K, V> m;
m.emplace(key, V(args));            // always constructs V
m.try_emplace(key, args...);         // only constructs V if key was absent

try_emplace (C++17) avoids unnecessary V construction on duplicates.

When to use which

  • Default to emplace when constructing in place and the ctor args are clear.
  • Use push_back(std::move(x)) when you have an existing object to put in.
  • Use push_back({a, b, c}) for braced syntax / initializer lists.

Counter-example — performance non-difference

For trivially-copyable types (int, small structs), emplace_back(x) and push_back(x) generate the same code. The “always faster” claim is wrong.


C++.28 — Small String Optimization (SSO)

The problem

std::string has three logical pieces: size, capacity, pointer. Heap-allocating the data for every string — even "hi" — is wasteful. SSO stores small strings inline in the string object itself.

Layout — libc++ (clever)

sizeof(std::string) on libc++ is 24 bytes (3 pointers). Two layouts share the same 24 bytes:

Long form:

[ptr (8B)] [size (8B)] [capacity (8B, high bit = 1)]

Short form (inline):

[chars[22] (22B)] [length (1B)] [marker (1B, high bit = 0)]

The discriminator is the high bit of the last byte. Capacity for short = 22 chars (plus null terminator); for long, up to whatever you allocate.

libstdc++ (simpler)

15-character SSO. Capacity is separately tracked; layout includes size, capacity, and a small buffer in the string object.

Why SSO matters

  • Most strings are short (“Hello”, filenames, JSON keys). Eliminating malloc cost is huge.
  • Cache locality — small strings live in the string object itself.
  • Trivial moves: short-string move is just memcpy of the inline bytes; no pointer-swap dance.

How operations behave

std::string s = "hi";          // short — no heap
s += " world";                  // still short (8 chars)
s.append(1000, 'x');            // grows to long — heap allocation

Switching short → long happens transparently. The string keeps track of which mode it’s in.

Move performance

std::string a = "x";
std::string b = std::move(a);   // short — copy bytes; a is left in some valid state

For short strings, moves are about as expensive as copies. For long strings, moves are pointer steals — much cheaper. So std::move(string) is sometimes a no-op gain.

Comparing implementations

ImplementationSSO capacitysizeof
libstdc++1532
libc++2224
MSVC1532
Boost::small_stringconfigurableconfigurable

SBO / small buffer optimization elsewhere

Same idea applied to:

  • std::function (stores small callables inline; falls back to heap for big ones).
  • std::any (small types stored inline).
  • boost::small_vector<T, N> and llvm::SmallVector<T, N>.

Counter-example — when SSO hurts

std::vector<std::string> v(1000000);     // sizeof(string)*1M = 24MB, most strings empty

If your strings are mostly long, SSO wastes space (24 B per string when only 16 B are used for {ptr, size, cap}). For workloads where strings are predominantly large, a custom string type with no SSO can be smaller.

Hidden cost

Every method that examines the string has to branch on short-vs-long mode. A modern CPU branch-predicts this perfectly, so it’s effectively free, but it’s there.


C++.29 — shared_from_this()

The problem

You’re inside a member function and you need to give someone a shared_ptr<T> to yourself. Naive:

class S {
public:
    void register_callback(callback_queue& q) {
        q.add(std::shared_ptr<S>(this));    // WRONG — creates a second control block
    }
};

You now have two unrelated control blocks each thinking they own *this. Whichever’s count hits 0 first deletes the object; the other has a dangling pointer.

The fix — enable_shared_from_this

class S : public std::enable_shared_from_this<S> {
public:
    std::shared_ptr<S> get_self() {
        return shared_from_this();
    }
};

shared_from_this() returns a shared_ptr<S> that shares the existing control block — so the count is consistent.

How it works

enable_shared_from_this contains a weak_ptr<T>. The first shared_ptr<T> constructor to receive a T* whose class derives from enable_shared_from_this<T> initializes that internal weak_ptr to itself. shared_from_this() then .lock()s the weak_ptr to produce a new shared_ptr.

Pre-condition

The object must already be owned by a shared_ptr before you call shared_from_this(). Otherwise the internal weak_ptr is empty.

auto p = std::make_shared<S>();
p->get_self();                          // OK

S s;
s.get_self();                           // throws bad_weak_ptr — never shared_ptr'd

C++17 fix for ctors

You cannot call shared_from_this() from a constructor — the shared_ptr doesn’t exist yet at that point. Workaround: factory function that constructs and then calls a init() virtual:

class S : public std::enable_shared_from_this<S> {
public:
    static std::shared_ptr<S> create() {
        auto p = std::make_shared<S>();
        p->init();          // now shared_from_this() works
        return p;
    }
private:
    void init() { /* now can use shared_from_this() */ }
};

When to use

  • Async callbacks that need to keep the object alive until they fire.
  • Self-registering listeners.
  • Code that hands ownership to multiple consumers.

Counter-example — capture by [this] in async

class Service {
public:
    void start_async() {
        std::async([this] { do_work(); });    // dangling if *this dies
    }
};

Fix:

std::async([self = shared_from_this()] { self->do_work(); });

The captured shared_ptr keeps *self alive for the duration of the task.


C++.30 — Memory order

The problem

Modern CPUs and compilers reorder instructions. Within a single thread the reorderings are invisible (the result is as if serial). Across threads they are not.

// Thread 1
data = 42;          // (1)
ready = true;       // (2)

// Thread 2
while (!ready) {}   // (3)
assert(data == 42); // (4)

Without synchronization, the compiler or hardware can reorder (1) and (2), or (3) and (4); thread 2 may see ready == true before data == 42 arrives. Assert can fire.

The C++ memory model

Atomics provide ordered atomic operations. The order parameter says what reorderings are forbidden around the op.

std::atomic<bool> ready;
int data;

// Thread 1
data = 42;
ready.store(true, std::memory_order_release);

// Thread 2
while (!ready.load(std::memory_order_acquire)) {}
assert(data == 42);   // safe

The six orderings

OrderMeaning
memory_order_relaxedatomic op, no ordering constraint on other ops
memory_order_consume(deprecated, treated as acquire)
memory_order_acquire(load) no later op may be reordered before this
memory_order_release(store) no earlier op may be reordered after this
memory_order_acq_rel(RMW) acquire + release
memory_order_seq_cst(load/store) global total order across all seq_cst ops

Release / Acquire pairing

Release-store on ready in thread 1 publishes all prior writes (including data = 42). Acquire-load on ready in thread 2 subscribes; if it sees the released value (true), it also sees everything that happened before the release.

This is the canonical pattern: “send a message” via atomic + plain data, “receive a message” via atomic + plain data.

Sequential consistency

seq_cst is the default for std::atomic<T>::store/load. Gives a global total order — all threads observe seq_cst ops in the same order.

// Thread 1: x.store(1); y.store(1);
// Thread 2: a = y.load(); b = x.load();
// With seq_cst: cannot observe a==1, b==0 (would imply reordering of T1's stores)

Provides the strongest guarantees but the most expensive — on x86 it forces store-buffer flush (MFENCE or LOCK-prefixed op) at every seq_cst store.

Relaxed

counter.fetch_add(1, std::memory_order_relaxed);

The increment is atomic (no torn writes), but no ordering with other reads/writes. Perfect for hit counters, stats — you want correctness of the counter but don’t care about ordering relative to other variables.

Acquire/Release on x86

x86 is a strong memory model: every load is implicit-acquire, every store is implicit-release. So memory_order_acquire/release on x86 are just normal loads/stores. Only seq_cst adds a fence.

On ARM/POWER (weaker models), acquire/release actually emit fences/barrier-like instructions (ldar/stlr on ARMv8). relaxed is cheaper.

Compare-and-swap

T expected = old;
while (!atom.compare_exchange_weak(expected, expected + 1,
                                    std::memory_order_acq_rel,
                                    std::memory_order_acquire)) {
    // failure: expected has been updated to actual; retry
}

Two memory orders: one for success, one for failure (which can be weaker, since on failure no write happens).

weak vs strong:

  • compare_exchange_weak may spuriously fail (on ARM LL/SC, transient cache events).
  • compare_exchange_strong retries internally; use when retry is expensive or you’re not in a loop.

In a CAS loop, weak is usually preferred (the loop handles spurious failure naturally).

Lock-free queue example

template<class T>
class lockfree_queue {
    struct Node { T value; std::atomic<Node*> next{nullptr}; };
    std::atomic<Node*> head, tail;
public:
    void push(T v) {
        auto* n = new Node{std::move(v), nullptr};
        Node* prev = tail.exchange(n, std::memory_order_acq_rel);
        prev->next.store(n, std::memory_order_release);
    }
    bool pop(T& out) {
        auto* h = head.load(std::memory_order_acquire);
        Node* nxt = h->next.load(std::memory_order_acquire);
        if (!nxt) return false;
        out = std::move(nxt->value);
        head.store(nxt, std::memory_order_release);
        delete h;
        return true;
    }
};

(Simplified; real lock-free queues need hazard pointers or epoch-based reclamation to avoid ABA.)

std::atomic_thread_fence

Standalone fence without any atomic op:

data = 42;
std::atomic_thread_fence(std::memory_order_release);
ready_flag = 1;          // ordinary write

Sometimes used in combination with relaxed atomics for fine-grained control. Hard to get right; mostly leave to library code.

Performance summary (x86-64, typical)

OpCost
relaxed load/storesame as plain load/store
acquire loadsame as plain load (x86 strong model)
release storesame as plain store
acq_rel fetch_add~5–10 ns uncontended
seq_cst store~20–30 ns (MFENCE/LOCK)
compare_exchange~10 ns uncontended; orders of magnitude under contention

ARM:

  • acquire/release use LDAR/STLR.
  • seq_cst is similar cost — total order across threads achieved via these.
  • relaxed is cheapest.

Choosing

PatternOrder
Counter, statisticsrelaxed
Producer publishes data + flagrelease / acquire
Sequential code that must see all events in same order globallyseq_cst
Spinlockacquire on lock, release on unlock
Reference counting (smart_ptr inc)relaxed
Reference counting (smart_ptr dec)acq_rel
Hazard pointers, RCUnuanced — read papers

Counter-example — relaxed where acquire is needed

std::atomic<int> ready{0};
int data;
// Thread 1
data = 42;
ready.store(1, std::memory_order_relaxed);   // BUG — no release
// Thread 2
while (ready.load(std::memory_order_relaxed) != 1) {}
assert(data == 42);   // may fire on ARM

On x86 this often “works” because of the strong memory model, but it’s UB-ish (no synchronizes-with edge). Move to release/acquire for portability.

Mental model

  • Acquire = “all my subsequent reads happen after this load.”
  • Release = “all my prior writes happen before this store.”
  • Acq-Rel = both, for read-modify-write.
  • Seq-Cst = “we all agree on a single ordering of these ops.”
  • Relaxed = atomic, but no ordering.

The C++ formal model is synchronizes-with / happens-before. Memorize the pattern: release-write paired with acquire-read forms a synchronizes-with edge, which forces visibility of all prior writes.


Comments