001 Notes
C++ Interview Notes
C++.1 — sizeof of an empty class
sizeof of an empty classclass Empty {};
static_assert(sizeof(Empty) == 1);
Empty Base Optimization (EBO)
class Empty {};
class Derived : Empty {
int x;
};
static_assert(sizeof(Derived) == 4); // EBO: empty base contributes 0
[[no_unique_address]] (C++20) — EBO for members
[[no_unique_address]] (C++20) — EBO for membersstruct Empty {};
struct S {
[[no_unique_address]] Empty e;
int x;
};
static_assert(sizeof(S) == 4); // e takes 0 bytes
Why size 1 and not 0
Empty a, b;
assert(&a != &b); // must hold
Counter-example — empty class with virtual function
class WithVirtual {
public:
virtual void f() {}
};
sizeof(WithVirtual); // 8 (or 4) — vptr added
What “empty” actually means for EBO
C++.2 — RAII
The idea
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
void process() {
FILE* f = fopen("/var/log/x", "r");
try {
if (something) throw std::runtime_error("oops");
fclose(f);
} catch (...) {
fclose(f);
throw;
}
}
Standard library RAII types
| Type | Resource |
|---|---|
std::unique_ptr | heap object |
std::shared_ptr | shared heap object |
std::lock_guard, std::scoped_lock, std::unique_lock | mutex |
std::fstream | OS file handle |
std::thread (with jthread C++20) | OS thread |
std::async’s future | thread + result slot |
std::vector, all containers | heap buffer |
The Rule of Five
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
};
class Logger {
std::unique_ptr<File> f;
std::vector<std::string> buffer;
}; // no special members needed; correct by construction
Initialization order
class Conn {
SocketHandle sock;
Encryptor enc; // depends on sock
public:
Conn(const std::string& host)
: sock(host)
, enc(sock) {} // sock guaranteed constructed first
};
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
Move + RAII
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); }
};
C++.3 — Lambda
What a lambda is
auto add = [](int a, int b) { return a + b; };
struct __lambda_xyz {
auto operator()(int a, int b) const { return a + b; }
};
__lambda_xyz add{};
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
struct __lambda {
int x; // copy
int& y; // reference
auto operator()() const { return x + y; }
};
Mutable
auto f = [n = 0]() mutable { return ++n; }; // generates non-const operator()
f(); f(); f(); // returns 1, 2, 3 — state persists per-lambda instance
Generic lambdas (C++14)
auto f = [](auto x, auto y) { return x + y; };
struct __lambda {
template<class A, class B>
auto operator()(A x, B y) const { return x + y; }
};
auto f = []<class T>(T x) { return T{x + x}; };
Conversion to function pointer
auto f = [](int x) { return x + 1; };
int (*fp)(int) = f; // OK
Storage and performance
std::sort(v.begin(), v.end(), [](int a, int b){ return a > b; });
Capturing this gotcha
this gotchastruct S {
int n = 5;
auto get_lambda() { return [this]{ return n; }; }
};
auto f = S{}.get_lambda(); // temporary S destroyed
f(); // UB — captured this dangling
auto get_lambda() { return [*this]{ return n; }; } // safe even after S dies
When not to use lambdas
C++.4 — Casting
static_cast<T>(expr)
static_cast<T>(expr)double d = static_cast<double>(42);
Base* b = ...;
Derived* d = static_cast<Derived*>(b); // UB if b is not actually a Derived
dynamic_cast<T>(expr)
dynamic_cast<T>(expr)Base* b = get_object();
if (auto* d = dynamic_cast<Derived*>(b)) {
d->derived_only_method();
}
const_cast<T>(expr)
const_cast<T>(expr)const int x = 5;
int& y = const_cast<int&>(x);
y = 10; // UB — x is actually const
reinterpret_cast<T>(expr)
reinterpret_cast<T>(expr)auto i = reinterpret_cast<std::uintptr_t>(ptr);
std::bit_cast<T>(src) (C++20)
std::bit_cast<T>(src) (C++20)float f = 1.5f;
std::uint32_t bits = std::bit_cast<std::uint32_t>(f);
C-style cast (T)expr
(T)exprFunctional-style T(expr)
T(expr)int(3.5); // C-style cast — same as (int)3.5
Widget(args); // construction — same as Widget{args}
Decision tree
| Need | Cast |
|---|---|
| Numeric conversion | static_cast |
| Up the hierarchy (Derived→Base) | implicit, or static_cast for clarity |
| Down the hierarchy, sure it’s right | static_cast |
| Down the hierarchy, want runtime check | dynamic_cast |
| Remove constness from API | const_cast (almost always wrong) |
| Type-punning floats/ints | std::bit_cast |
| Tagged pointers, address arithmetic | reinterpret_cast |
| Anything else | reconsider design |
C++.5 — Struct vs class
The technical difference
class C { int x; }; // x is private
struct S { int x; }; // x is public
class D : Base {}; // private inheritance
struct E : Base {}; // public inheritance
The convention
| Use | Pattern |
|---|---|
struct | Aggregate / data-bag with no invariants (struct Point { int x, y; };) |
class | Type with private state and public interface enforcing invariants |
Aggregate types
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)
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
POD vs trivial vs aggregate
static_assert(std::is_trivially_copyable_v<Point>);
static_assert(std::is_standard_layout_v<Point>);
When to flip the convention
C++.6 — new vs normal creation
new vs normal creationStack creation
Widget w(args);
new T(args)
new T(args)Widget* p = new Widget(args);
delete p; // calls ~Widget(), then operator delete
Allocation hooks
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); }
class Pool {
static void* operator new(std::size_t);
static void operator delete(void*);
};
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!
Nothrow new
Widget* p = new (std::nothrow) Widget(args);
if (!p) { /* allocation failed */ }
new[] / delete[]
new[] / delete[]auto* p = new Widget[10];
delete[] p; // runs 10 destructors then operator delete[]
Why prefer stack and smart pointers
C++.7 — new T vs new T() vs new T{}
new T vs new T() vs new T{}The three forms
| Form | Initialization kind |
|---|---|
new T | default-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 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 ()
Aggregate brace init
struct Point { int x, y; };
auto* p = new Point{1, 2}; // x=1, y=2 — aggregate init
Narrowing rules
int n = 1000;
new char(n); // OK — narrows silently
new char{n}; // error — narrowing from int to char
Most-vexing parse
Widget w(); // declares a function w() returning Widget — NOT an object
Widget w{}; // creates a Widget, no ambiguity
std::vector example
std::vector examplestd::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
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
C++.8 — Destruction vs deallocation
Destruction
Deallocation
delete p does both
delete p does bothdelete 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
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
std::destroy_at (C++17)
std::destroy_at (C++17)std::destroy_at(p); // same as p->~T(), but works in templates
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
}
};
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 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
+----------+
| B::A::x | <-- B's A subobject
+----------+
| (B data) |
+----------+
| C::A::x | <-- C's A subobject
+----------+
| (C data) |
+----------+
| (D data) |
+----------+
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
Cost of virtual inheritance
+-----------+
| B vbptr | -> points to vtable describing offset to A
| (B data) |
+-----------+
| C vbptr | -> ditto
| (C data) |
+-----------+
| (D data) |
+-----------+
| A subobj | <-- the single A
+-----------+
Who initializes the virtual base?
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
};
Should you use it?
C++.10 — Virtual inheritance
What virtual on a base means
virtual on a base meansclass Derived : virtual Base { ... };
Memory layout typical (Itanium ABI)
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; };
[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
Conversion costs
D d;
B* pb = &d; // ok, no cost
A* pa = pb; // needs vbptr lookup to get the actual A address
Constructor order
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;
};
Use cases
C++.11 — Inheritance via TMP (template metaprogramming)
Why static polymorphism
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(); }
When CRTP wins
When CRTP loses
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);
}
};
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{}); }
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(); }
C++.12 — Aliasing
What aliasing means
Strict aliasing rule
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
Legitimate type-punning
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)
restrict in C++
restrict in C++C++.13 — Casting vs std::launder
std::launderThe problem std::launder solves
std::launder solvesstruct 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
std::launder (C++17)
std::launder (C++17)int x = std::launder(&a)->n; // 2 — safe
When you need launder
launderStandard library hides it for you
std::optional<T> opt;
opt.emplace(args); // does placement new internally with launder where needed
Cast vs launder — when each applies
| Situation | Tool |
|---|---|
| Convert between types of an existing object | static_cast, dynamic_cast, etc. |
Get an int from a float bit-pattern | std::bit_cast |
| Treat raw memory as an object you just placement-new’d | std::launder |
| Pointer arithmetic, low-level access | reinterpret_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
C++.14 — noexcept and std::vector move behavior
noexcept and std::vector move behaviorThe exception guarantee of vector operations
vector operationspush_back and reallocation
push_back and reallocationThe rule
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 {}
};
Verify
static_assert(std::is_nothrow_move_constructible_v<Fast<int>>);
static_assert(!std::is_nothrow_move_constructible_v<Slow<int>>);
Why moves are usually noexcept
noexceptCounter-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;
}
};
C++.15 — Forwarding reference
What it is
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&&
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
Reference collapsing
| Combine | Result |
|---|---|
T& & | T& |
T& && | T& |
T&& & | T& |
T&& && | T&& |
std::forward<T>(x)
std::forward<T>(x)template<class T>
void wrapper(T&& arg) {
callee(std::forward<T>(arg));
}
template<class T>
T&& forward(std::remove_reference_t<T>& x) noexcept {
return static_cast<T&&>(x);
}
Why std::move is different
std::move is differenttemplate<class T>
std::remove_reference_t<T>&& move(T&& x) noexcept {
return static_cast<std::remove_reference_t<T>&&>(x);
}
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)...));
}
Common bug — using move instead of forward
move instead of forwardtemplate<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 in lambdas
auto f = [](auto&& x) {
callee(std::forward<decltype(x)>(x));
};
C++.16 — Implement unique_ptr, shared_ptr
unique_ptr, shared_ptrunique_ptr — single-owner
unique_ptr — single-ownertemplate<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; }
};
make_unique
make_uniquetemplate<class T, class... Args>
unique_ptr<T> make_unique(Args&&... args) {
return unique_ptr<T>(new T(std::forward<Args>(args)...));
}
shared_ptr — shared ownership with reference count
shared_ptr — shared ownership with reference countshared_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)
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
make_shared vs shared_ptr(new T)
make_shared vs shared_ptr(new T)[control_block_inplace][T data...]
weak_ptr
weak_ptrtemplate<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;
}
};
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
Atomic shared_ptr operations
C++.17 — Virtual destructors
Rule
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
Why it’s special
Pure virtual destructor
struct Interface {
virtual ~Interface() = 0; // pure virtual
};
Interface::~Interface() {} // MUST provide a definition
When to avoid virtual dtor
final for polymorphic types
final for polymorphic typesclass Impl final : public Interface { ... };
C++.18 — Destructor throws during stack unwinding
The trap
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
Detecting unwinding
struct Logger {
~Logger() {
if (std::uncaught_exceptions() > 0) {
// we're being destroyed during exception propagation
}
}
};
Best practice
struct Conn {
~Conn() noexcept try {
send_close();
} catch (...) {
log("close failed");
// do not rethrow
}
};
Standard library guarantees
C++.19 — Delegating constructor
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
Order of construction
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
C++.20 — Inheriting constructor
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
Derived(P... args) : Base(std::forward<P>(args)...) {}
Caveats
When to use
C++.21 — Default vs value vs zero vs aggregate initialization
The terminology
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
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
Zero initialization
Direct initialization
T x(args);
T x{args};
new T(args)
T(args)
Copy initialization
T x = expr;
T x = {a, b};
f(expr); // passing argument
return expr; // returning value
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
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)
Point d{1}; // x=1, y=0
Point e{}; // x=0, y=0
The big table
| Syntax | For built-in | For class with default ctor | For aggregate |
|---|---|---|---|
T x; | uninit | calls ctor | calls ctor (or uninit members) |
T x{}; | 0 | calls ctor | zero-init members |
T x(); | declares function! | declares function! | declares function! |
T() | prvalue, value-init | prvalue, value-init | prvalue, value-init |
new T | uninit | calls ctor | uninit members |
new T() | 0 | calls ctor | zero-init members |
new T{} | 0 | calls ctor | zero-init members |
C++.22 — vtable vs vptr
What they are
Layout example
struct Animal {
virtual void speak() = 0;
virtual ~Animal() = default;
int age;
};
struct Dog : Animal {
void speak() override;
void fetch();
int breed_id;
};
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 |
+----------+
load vptr from *animal_ptr
load function pointer from vtable[speak_slot]
call function pointer
Where vptr is set
struct Base {
Base() { speak(); } // calls Base::speak — even from Derived
virtual void speak() {}
};
struct Derived : Base {
void speak() override {}
};
Derived d; // prints Base::speak
Why dynamic_cast costs more
Size cost
Multiple inheritance
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
Devirtualization
class Dog final : public Animal { ... };
C++.23 — override keyword
override keywordstruct 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
final is the bookend
final is the bookendstruct 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
class B final : public A { ... }; // B itself can't be inherited from
Style: always use override
overridestruct Derived : Base {
void foo() override; // ✓
};
struct Derived : Base {
virtual void foo(); // works, but is the redundant `virtual` actually overriding?
};
C++.24 — dynamic_cast
dynamic_castWhat it does
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
| Form | Failure |
|---|---|
dynamic_cast<T*>(p) | returns nullptr |
dynamic_cast<T&>(ref) | throws std::bad_cast |
Implementation
if (typeid(*a) == typeid(Dog))
// a's dynamic type is exactly Dog
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
When to use
When NOT to use
Counter-example — types without virtuals
struct Animal {};
struct Dog : Animal {};
Animal* a = new Dog;
Dog* d = dynamic_cast<Dog*>(a); // error: Animal not polymorphic
C++.25 — decltype, std::enable_if, std::integral_constant, constexpr
decltype, std::enable_if, std::integral_constant, constexprdecltype
decltypeint x = 5;
decltype(x) y = 10; // int
auto f() -> decltype(some_expr) { ... } // trailing return type
int n;
decltype(n) a; // int — name lookup
decltype((n)) b; // int& — parenthesized expression is an lvalue
std::enable_if (SFINAE)
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; }
template<std::integral T>
T abs(T v) { return v < 0 ? -v : v; }
std::integral_constant
std::integral_constantusing True = std::integral_constant<bool, true>;
True::value; // true
True{}(); // true (operator())
constexpr
constexprconstexpr 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
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);
C++.26 — std::allocator, std::pmr::memory_resource
std::allocator, std::pmr::memory_resourceThe classic Allocator interface
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
std::pmr::memory_resource (C++17)
std::pmr::memory_resource (C++17)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;
};
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
Chaining
std::pmr::monotonic_buffer_resource arena{1 << 16}; // upstream = new_delete_resource
std::pmr::unsynchronized_pool_resource pool{&arena};
Performance
C++.27 — emplace_back vs push_back
emplace_back vs push_backpush_back
push_backv.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
emplace_backv.emplace_back(1, 2); // constructs Widget(1, 2) directly in the buffer
When emplace wins
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
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]
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
try_emplace / emplace for map
try_emplace / emplace for mapstd::map<K, V> m;
m.emplace(key, V(args)); // always constructs V
m.try_emplace(key, args...); // only constructs V if key was absent
When to use which
C++.28 — Small String Optimization (SSO)
The problem
Layout — libc++ (clever)
[ptr (8B)] [size (8B)] [capacity (8B, high bit = 1)]
[chars[22] (22B)] [length (1B)] [marker (1B, high bit = 0)]
libstdc++ (simpler)
Why SSO matters
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
Move performance
std::string a = "x";
std::string b = std::move(a); // short — copy bytes; a is left in some valid state
Comparing implementations
| Implementation | SSO capacity | sizeof |
|---|---|---|
| libstdc++ | 15 | 32 |
| libc++ | 22 | 24 |
| MSVC | 15 | 32 |
| Boost::small_string | configurable | configurable |
SBO / small buffer optimization elsewhere
Counter-example — when SSO hurts
std::vector<std::string> v(1000000); // sizeof(string)*1M = 24MB, most strings empty
C++.29 — shared_from_this()
shared_from_this()The problem
class S {
public:
void register_callback(callback_queue& q) {
q.add(std::shared_ptr<S>(this)); // WRONG — creates a second control block
}
};
The fix — enable_shared_from_this
enable_shared_from_thisclass S : public std::enable_shared_from_this<S> {
public:
std::shared_ptr<S> get_self() {
return shared_from_this();
}
};
How it works
Pre-condition
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
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
C++.30 — Memory order
The problem
// Thread 1
data = 42; // (1)
ready = true; // (2)
// Thread 2
while (!ready) {} // (3)
assert(data == 42); // (4)
The C++ memory model
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
| Order | Meaning |
|---|---|
memory_order_relaxed | atomic 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
Sequential consistency
// 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)
Relaxed
counter.fetch_add(1, std::memory_order_relaxed);
Acquire/Release on x86
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
}
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;
}
};
std::atomic_thread_fence
std::atomic_thread_fencedata = 42;
std::atomic_thread_fence(std::memory_order_release);
ready_flag = 1; // ordinary write
Performance summary (x86-64, typical)
| Op | Cost |
|---|---|
relaxed load/store | same as plain load/store |
acquire load | same as plain load (x86 strong model) |
release store | same 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 |
Choosing
| Pattern | Order |
|---|---|
| Counter, statistics | relaxed |
| Producer publishes data + flag | release / acquire |
| Sequential code that must see all events in same order globally | seq_cst |
| Spinlock | acquire on lock, release on unlock |
| Reference counting (smart_ptr inc) | relaxed |
| Reference counting (smart_ptr dec) | acq_rel |
| Hazard pointers, RCU | nuanced — 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
Comments