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 exceptionRAII 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 want | Use |
|---|---|
| Sole owner | unique_ptr<T> |
| Shared owner | shared_ptr<T> |
| Non-owning observer | T* or T& (or weak_ptr) |
| Owned value | plain 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
| Need | Container |
|---|---|
| Default sequence | vector (contiguous, cache-friendly) |
| Fast front+back insert | deque |
| Key→value, avg O(1) | unordered_map |
| Sorted, ordered iteration | map / set |
| Stable element addresses | list (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
noexceptfunction 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_ptrby default;shared_ptronly for genuine shared ownership- Pass cheap types by value, others by
const&; return by value (moves/RVO) autofor obvious types;constby default;constexprwhere it can be- Prefer the STL algorithm to a hand-written loop
overrideon every override;expliciton 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
| Type | Pass as |
|---|---|
| Small/cheap (int, pointer, span) | by value |
| Big, read-only | const T& |
| Modified in place | T& |
| Sink (you'll store it) | by value, then std::move |
| String, read-only | std::string_view |
| Contiguous range, read-only | std::span<const T> |