Knowledge/C++/Introduction
28 Chapters
C++20 Baseline
Living Document
Free License
Engineering Publication · Syed Omar Ibn Feroj← Back to portfolio
++
Engineering Notes · Open Knowledge Repository

C++ — Engineering
Notes for Modern Code

A working reference for the C++ worth writing today — RAII as the organising principle, value semantics and moves, templates without fear, and the STL that means you rarely write a loop.

28
Chapters
C++20
Baseline
Living
Document
Free
License
Ch. 01
Build & Model
Compilation units, ODR, and why C++ builds are slow.
1.1 — One Definition Rule

Every entity may be declared many times but defined exactly once across the program (per-TU for inline/templates). Violating the ODR is undefined behaviour the linker often doesn't catch.

Shell
$ g++ -std=c++20 -Wall -Wextra -O2 a.cpp -o app
Headers expand into every translation unit — a heavy header included widely is why builds crawl. Forward-declare where possible; put definitions in .cpp; consider modules (C++20) for new code.
Ch. 02
Types & auto
Value types, auto deduction, and when it surprises you.
2.1 — auto strips references and const by default
C++
const std::string& name();
auto  a = name();   // std::string — a COPY
const auto& b = name();  // no copy, keeps const ref
auto&& c = name();  // forwarding ref — binds anything
Plain auto x = expr; deduces a value type and copies. For "I just want a name for this object", write const auto& — a silent copy of a big container in a hot loop is a classic perf bug.
Ch. 03
References
lvalue refs, rvalue refs, and dangling.
3.1 — A reference is an alias, not a pointer
C++
int x = 1;
int& r = x;     // r IS x; no null, no rebind
const int& cr = 42;  // const ref can bind a temporary
Returning a reference (or string_view) to a local or to a temporary is a dangling reference — use-after-free dressed up. The bug often "works" until an optimisation or unrelated change shifts the stack.
Ch. 04
const & constexpr
Compile-time guarantees vs compile-time computation.
4.1 — constexpr runs at compile time
C++
constexpr int sq(int n) { return n * n; }
constexpr int N = sq(8);   // 64, computed by the compiler
int arr[N];                  // usable as a constant expression
Prefer constexpr over #define for constants — it is typed, scoped, and visible to the debugger. Macros are textual and lie to every tool you own.
Ch. 05
Functions
Overloading, default args, trailing return types.
5.1 — Overload resolution picks the best match
C++
void f(int);
void f(double);
f(3);     // f(int)
f(3.0);   // f(double)
// f(3L);  ← ambiguous if both need a conversion
Default arguments are bound at the call site by static type, and overloading + defaults together produce surprising ambiguities. Keep overload sets small and obvious.
Ch. 06
Namespaces
Organising names; why 'using namespace' belongs nowhere near a header.
6.1 — Scope, alias, and ADL
C++
namespace app::net { int send(); }
namespace an = app::net;   // alias
an::send();
using namespace std; in a header injects the entire std namespace into every file that includes it, creating ambiguities that surface far away. Never put a using-directive at namespace scope in a header.
Ch. 07
Classes
Members, constructors, initialiser lists, the explicit keyword.
7.1 — Initialise in the member-init list, in declaration order
C++
class Box {
  int w_, h_;
public:
  explicit Box(int w, int h) : w_(w), h_(h) {}
  int area() const { return w_ * h_; }
};
Members initialise in declaration order, not init-list order. Listing them out of order is a common warning-level bug. Mark single-arg constructors explicit to stop silent conversions.
Ch. 08
RAII
The single most important idea in C++.
8.1 — Resource lifetime = object lifetime
C++
{
  std::lock_guard lk(mtx);   // acquires here
  std::ofstream f("out");    // opens here
  // ... may throw ...
} // f closed, mtx unlocked — automatically, even on exception
RAII makes leaks and unbalanced acquire/release structurally impossible: the destructor runs on every path out of the scope, including exceptions. Every raw resource (memory, file, lock, socket) should live inside an RAII type.
Ch. 09
Rule of Five / Zero
If you write one special member, think about all five.
9.1 — Prefer Rule of Zero
C++
// Rule of Zero: own nothing raw → compiler-generated members are correct
class User { std::string name; std::vector<int> ids; };
// copy/move/destroy all just work — no special members needed
If a class manages a raw resource you must define (or = delete/= default) the destructor, copy ctor, copy assign, move ctor, move assign — the Rule of Five. Far better: don't own raw resources, hold members that already do (Rule of Zero).
Ch. 10
Move Semantics
Transfer ownership instead of copying.
10.1 — std::move is a cast, not a move
C++
std::vector<int> a = make();
std::vector<int> b = std::move(a);  // steals a's buffer
// a is now valid-but-unspecified: don't read it, only reassign/destroy
std::move doesn't move anything — it casts to an rvalue so a move constructor can be selected. Using a moved-from object's value (beyond reassign/destroy) is a logic bug, not a compile error.
Ch. 11
Operator Overloading
Make value types behave like built-ins — tastefully.
11.1 — Symmetric operators as free functions
C++
struct Vec { double x, y; };
Vec operator+(Vec a, Vec b) { return {a.x+b.x, a.y+b.y}; }
auto operator<=>(const Vec&) const = default;  // all comparisons
Only overload an operator when the meaning is unambiguous (arithmetic on a math type). Cute operator abuse (+ for "add to list") makes code unreadable. The C++20 <=> generates all six comparisons from one line.
Ch. 12
Inheritance
Public inheritance is 'is-a'; prefer composition otherwise.
12.1 — Virtual destructor for polymorphic bases
C++
struct Base { virtual ~Base() = default; };
struct Derived : Base { /* ... */ };
std::unique_ptr<Base> p = std::make_unique<Derived>();
Deleting a derived object through a base pointer with a non-virtual destructor is undefined behaviour (the derived destructor never runs → resource leak). A polymorphic base needs a virtual destructor.
Ch. 13
Polymorphism
virtual, override, final — and slicing.
13.1 — Always write override
C++
struct Shape { virtual double area() const = 0; };
struct Circle : Shape {
  double area() const override;   // compiler verifies it overrides
};
Assigning a derived object to a base value (not reference/pointer) slices off the derived part. Pass polymorphic objects by reference or smart pointer, never by value.
Ch. 14
Smart Pointers
unique_ptr by default, shared_ptr when ownership is genuinely shared.
14.1 — make_unique / make_shared
C++
auto u = std::make_unique<Widget>(args);  // sole owner
auto s = std::make_shared<Widget>(args);  // refcounted
std::weak_ptr<Widget> w = s;             // breaks cycles
Two shared_ptrs that own each other never reach refcount 0 — a leak. Break ownership cycles with weak_ptr. Default to unique_ptr; shared_ptr has atomic-refcount cost and is overused.
Ch. 15
Ownership
Express who owns what in the type system.
15.1 — The ownership vocabulary
You wantUse
Sole ownerunique_ptr<T>
Shared ownershared_ptr<T>
Non-owning observerT* or T& (or weak_ptr)
Owned valueplain T by value
A raw pointer in modern C++ should mean "I observe but do not own". Ownership is always expressed by a smart pointer or by value — never by a raw new/delete pair.
Ch. 16
Templates
Compile-time generics — instantiated per type used.
16.1 — Function and class templates
C++
template <typename T>
T max(T a, T b) { return a > b ? a : b; }

template <typename T, std::size_t N>
struct Array { T data[N]; };
Templates are duck-typed and only checked when instantiated, so errors appear at the call site with famously long messages. Concepts (next chapter) move the error to the definition with a readable message.
Ch. 17
Concepts
Constrain templates; get human-readable errors (C++20).
17.1 — requires and named concepts
C++
template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;

Number auto add(Number auto a, Number auto b) { return a + b; }
A concept turns "no operator< for type X, 400 lines of instantiation backtrace" into "constraint Number not satisfied by X". Constrain every non-trivial template.
Ch. 18
Variadic Templates
Type-safe variable arguments via parameter packs.
18.1 — Fold expressions
C++
template <typename... Ts>
auto sum(Ts... xs) { return (xs + ...); }  // C++17 fold
sum(1, 2, 3.5);   // fully type-checked, no va_list
Variadic templates are the type-safe replacement for C's va_list — the compiler checks every argument. This is how std::format and make_unique forward arguments.
Ch. 19
Lambdas
Closures — capture by value, by reference, carefully.
19.1 — Capture lists decide lifetime
C++
int n = 10;
auto byval = [n](int x) { return x + n; };  // copies n
auto byref = [&n](int x) { return x + n; }; // refers to n
A by-reference capture that outlives the captured variable (stored in a member, returned, scheduled on a thread) dangles. Default to capture-by-value for anything that escapes the current scope.
Ch. 20
Containers
Pick by access pattern, not by habit.
20.1 — The decision table
NeedContainer
Default sequencevector (contiguous, cache-friendly)
Fast front+back insertdeque
Key→value, avg O(1)unordered_map
Sorted, ordered iterationmap / set
Stable element addresseslist (rarely the right answer)
Reach for vector by default — even for "lots of inserts", a vector usually beats list because cache locality dominates algorithmic complexity at realistic sizes.
Ch. 21
Iterators
The glue between containers and algorithms — and invalidation.
21.1 — Invalidation is the trap
C++
std::vector<int> v = {1,2,3};
for (auto it = v.begin(); it != v.end(); ) {
  if (*it % 2) it = v.erase(it);  // erase returns next valid
  else ++it;
}
A vector reallocation (e.g. push_back past capacity) invalidates all iterators/pointers/references into it. Erasing invalidates from the erase point. Using a stale iterator is UB; always rebind from the value erase/insert returns.
Ch. 22
Algorithms
The STL means you almost never write a raw loop.
22.1 — Express intent, not mechanics
C++
#include <algorithm>
std::sort(v.begin(), v.end());
auto it = std::find_if(v.begin(), v.end(), [](int x){return x>10;});
int total = std::accumulate(v.begin(), v.end(), 0);
v.erase(std::remove(v.begin(), v.end(), 0), v.end());  // erase-remove
std::remove doesn't remove — it shuffles kept elements forward and returns the new logical end. You must call erase to actually shrink. C++20 std::erase(v, 0) does both.
Ch. 23
Ranges
Composable, lazy pipelines (C++20).
23.1 — Views compose without copying
C++
#include <ranges>
namespace rv = std::views;
auto r = v | rv::filter([](int x){return x%2;})
        | rv::transform([](int x){return x*x;});
// nothing computed until you iterate r
Ranges replace the verbose iterator-pair calls with readable, lazy pipelines — and views don't allocate. The materialisation cost is paid once, only on iteration.
Ch. 24
Strings & string_view
Owning vs non-owning text.
24.1 — string_view is a borrow
C++
void log(std::string_view s);   // no copy, accepts both
log("literal");
log(some_std_string);
string_view does not own its characters. Returning one that points into a temporary or a since-destroyed string is a dangling view — a use-after-free. Use it for parameters, not for storage.
Ch. 25
Exceptions
Throw on the exceptional; provide strong guarantees where you can.
25.1 — Throw by value, catch by const reference
C++
try {
  throw std::runtime_error("boom");
} catch (const std::exception& e) {
  std::cerr << e.what();
}
An exception that escapes a noexcept function (including most destructors) calls std::terminate. Destructors must not throw. RAII (Ch. 08) is what makes exception-safe code automatic.
Ch. 26
optional / variant / expected
Model 'maybe', 'one of', and 'value or error' in the type system.
26.1 — Make absence and failure explicit
C++
std::optional<User> find(int id);
if (auto u = find(1)) use(*u);

std::expected<Config, Error> load();  // C++23: value-or-error
Returning optional/expected forces the caller to handle the missing/error case at compile time — far safer than sentinel values (-1, nullptr) or out-params.
Ch. 27
Undefined Behaviour
C++ has all of C's UB plus its own.
27.1 — The C++-specific ones
  • Using a moved-from object's value
  • Dangling reference / string_view / iterator
  • Calling a virtual through a destroyed object
  • Data race (no atomics/locks on shared mutable state)
  • Throwing out of a noexcept function or a destructor
  • Out-of-lifetime access (use before construction / after destruction)
Same rule as C: the optimiser assumes UB never happens. Build tests under -fsanitize=address,undefined; it catches the majority of these at the point of the violation.
Ch. 28
Modern Guidelines
The defaults that keep modern C++ sane.
28.1 — The short list
  • RAII for every resource; Rule of Zero for every class that can manage it
  • unique_ptr by default; shared_ptr only for genuine shared ownership
  • Pass cheap types by value, others by const&; return by value (moves/RVO)
  • auto for obvious types; const by default; constexpr where it can be
  • Prefer the STL algorithm to a hand-written loop
  • override on every override; explicit on single-arg constructors
  • Build -Wall -Wextra, test under ASan + UBSan, run clang-tidy
REF
C++ Cheatsheet
Parameter passing, the decision that's always asked.
How to pass a parameter
TypePass as
Small/cheap (int, pointer, span)by value
Big, read-onlyconst T&
Modified in placeT&
Sink (you'll store it)by value, then std::move
String, read-onlystd::string_view
Contiguous range, read-onlystd::span<const T>