Basic Prerequisites: 03 · Memory & Cache and 02 · Why C++.
1. Intuition & Practical Objective
“Zero-cost abstraction” is Stroustrup’s promise: a well-designed C++ abstraction should cost nothing at runtime compared with the hand-written code it replaces. That promise is not automatic - it is a property you engineer, and this page covers the three tools that deliver it: templates (compile-time dispatch, no vtable), move semantics (transfer ownership instead of copying), and pre-allocated storage (no allocator in the hot path).
The central tension: readability wants abstraction (a generic Signal<Fast> type, a PriceSource interface); latency wants none of the runtime machinery abstraction usually drags in (virtual calls, heap allocation, refcount atomics). C++ resolves it by making those mechanics compile-time: the template specialises for the concrete type, the call is inlined, and the binary contains no runtime dispatch at all.
Three “aha”s:
- Abstraction can be free or expensive - you choose which.
template<class T> T square(T x){return x*x;}withT=doublecompiles to a single multiply. Avirtual double square(double)compiles to a vtable load + indirect call that may mispredict. Same source intent, different machine code. - Move ≠ copy. Copying elements is ; moving is (a few pointer writes). Returning a big buffer by value is only cheap because move makes it a pointer handoff.
- The allocator is the tax collector of “convenient” code. Every implicit
new(astd::vectorgrowing, astd::functionboxing a lambda, ashared_ptrcreating a control block) adds run time the source line conceals. Zero-cost engineering removes the call, not just the frequency.
One-sentence essence. “Write code as if abstraction were free, then verify with the profiler that it actually is - and where it isn’t, move the cost to compile time or out of the hot path.”
2. Mathematical Ground Truth & Derivations
2.1 Templates: dispatch cost collapses to zero
A virtual call traverses: load vptr → load function pointer from vtable → indirect call (often mispredicted, ~15–20 cycles flush) → execute. Its cost is
whereas a template instantiation resolves the concrete function at compile time and inlines it:
In a hot loop running times, the difference is - pure, avoidable overhead. Templating also unlocks inlining across the call boundary, which is what lets the compiler auto-vectorise (03 · Memory & Cache, §2.3).
2.2 Move semantics:
Copying a container of elements of size costs
so . A move constructor transfers the internal pointer, capacity, and size instead:
independent of . This is why return big_vector; is cheap (return-value optimisation and/or move) and why std::vector<std::vector<double>> operations are pointer-hops, not data floods. A shared_ptr copy is a special trap: it is not data, but it is an atomic refcount increment
i.e., a hidden synchronisation point on the hot path - prefer unique_ptr or, better, value types.
2.3 Allocation: amortised vs bounded (the reserve inequality)
Appending elements to a doubling vector triggers reallocations when capacity is exhausted. Element copies total
so the amortised per-append cost is - but the worst single append copies up to elements at an unpredictable moment. Reserve in advance and both terms vanish:
The tail paragraph is the point: amortised and bounded are different guarantees, and the hot path needs the second (this is the theme of 05 · Failure Modes).
2.4 The per-tick allocation tax
If a hot path allocates once per message at rate , the expected allocator time per second is , with as in 02 · Why C++, §2.3. §3 measures the fast-path component of that term in a language-agnostic proxy: the reused buffer is strictly cheaper than fresh allocation by a factor of ~3×.
3. Computational Implementation - allocation cost, measured
We cannot call a C++ allocator from these stdlib-only examples, but the mechanism (ask the runtime for a fresh buffer vs write into existing storage) is language-agnostic and reproduces the same order of magnitude. The example times both patterns, warm, over iterations.
Reading the result. Reusing pre-allocated storage is cheaper per operation (26.8 vs 90.4 ns/op) for identical work - the difference is entirely allocator/runtime overhead. The absolute numbers are interpreter-specific, but the ratio and the direction are the C++ rule stated as arithmetic: allocate once before the hot path, then write into the buffer. (In C++ the same experiment with std::vector + reserve vs push_back shows the §2.3 inequality directly - see 05 · Failure Modes for the growth-cost model.)
C++ form (prose). The engine that matches this experiment: a std::vector<Price> book; book.reserve(MAX_LEVELS); sized before the session starts, then book[i] = p; (or emplace_back bounded by the reserve) inside the callback - no new, no push_back past capacity, no std::function boxing. Templates specialise the strategy’s on_tick<Venue> per venue so the dispatch is inlined; moves pass the order batch between components as a pointer handoff.
4. Failure Modes & First-Principles Breakdowns
- The abstraction that isn’t zero-cost.
std::functionmay heap-allocate its target; virtual calls mispredict;shared_ptrcopies do atomic refcounts. Zero-cost is a property to verify (inspect the assembly / profile), not a property to assume. Prefer templates,unique_ptr/value types, andconst&. - Allocating in the hot path. The §3 gap plus the growth spike (§2.3) are why every production hot path is pre-allocated and pool-based. A single
push_backpast capacity can memcpy hundreds of thousands of elements at an arbitrary instant. - Move mistakes. Forgetting
std::move(silent copy) or moving from an object you then use (moved-from state is unspecified) and - the subtle one - capturing by value in a lambda that then copies a large container. Move correctness is a lifetime discipline, not just a speed trick. - Rule-of-five erosion. A type with a user-declared destructor but no move constructor loses its implicit move and silently copies. This is a classic hidden-regression source; follow the rule of five (or zero) rigorously.
- Template bloat / compile time. Zero runtime cost is not zero build cost; heavy instantiation balloons the binary and compile time, and code-bloat can hurt the instruction cache. Specialise deliberately, not indiscriminately.
5. References
- Meyers, Scott: Effective Modern C++
- Meyers, Scott: Effective C++
- Stroustrup, Bjarne: The C++ Programming Language / A Tour of C++
- Ghosh, Sourav: Building Low Latency Applications with C++
6. Connected Graph Bridges
- Back: 03 · Memory & Cache · Index Hub
- Continue: 05 · Failure Modes · 06 · Advanced Extensions
- Sibling: Concurrency & Lockless Programming (atomics vs locks; the cost model of
shared_ptrrefcounts) - Base: Numerical Methods (floating-point precision when choosing value types)