Skip to content

The cost cheat sheet for C++ abstractions

This article is ch06's reference card: it rolls the C++ abstraction costs measured in earlier articles into a cheat sheet, then adds three small entries that didn't get their own article (variable storage types, bitfields, enum class). When you hit "is this abstraction expensive" while coding, look it up here.

What was measured: the cost cheat sheet

AbstractionMain costMeasured number (this machine)When you care
Virtual function (via pointer)vtable lookup + indirect jump + blocks inlining0.55 ns, 2.5x CRTPA hot virtual call that hasn't been devirtualized
DevirtualizationOften free when the compiler can prove the typeDirect object 0.23 ns ≈ CRTPMost of the time the compiler does it for you
Exception (normal path)Table-driven zero cost0.25 ns (same as a pure function)Almost never
Exception (throwing path)EH table lookup + stack unwind857 ns, ~3400xKeep the exception path out of hot loops
std::function callType-erased indirect call1.61 ns, 6x a direct lambdaA million calls per frame
std::function constructionSmall hits SBO, big heap-allocatesSBO 2.3 ns / heap 19.6 ns (8.5x)Repeated construction + big capture on the hot path
RVO/NRVOReturn value constructed directly in the caller0 copies, 0 movesDon't write std::move on a return of a local
return std::move(local)Disables NRVO, forces a move0 copies + 1 move (one extra)Anti-pattern, don't write it

This table is the measurement roll-up of ch06-01/02/03/05; see each article for mechanism and experiment. The thesis (Carruth No Zero-Cost Abstractions): every C++ abstraction maps to a hardware cost, but "has a cost" doesn't equal "happens every time", and the compiler often eliminates it for you (devirtualization, zero-cost exceptions, RVO). Measure first, then decide whether to hand-write around it.

Supplementary entries

1. Variable storage types: register / static / thread_local

A variable's storage type affects where it lives and how fast it is to access (Agner vol1 §7.1):

  • Automatic variables (stack): the default. Fastest access (on a stack that fits in L1), and the compiler can put it in a register. The register keyword is meaningless on modern compilers (the compiler allocates registers itself); it's a deprecated/removed keyword since C++17, don't use it.
  • Static variables (static/global): fixed address, fixed initialization (constant initialization is zero-cost; dynamic initialization has a startup cost). In multithreaded code, the initialization of a static local is thread-safe (magic statics), but there's a runtime cost to thread-safe initialization (an atomic check on first entry).
  • thread_local: one per thread. Access is slightly more expensive (has to look up the thread-local storage area, usually a few extra instructions), but avoids sharing under multithreading. Useful for "per-thread context objects".

In practice: hot-path variables should be automatic where possible (let the compiler put them in registers); static global constants are free; thread_local is for per-thread context (its initialization and destruction cost has to be counted into the thread lifecycle).

2. Bitfields

A bitfield packs several small fields into one integer, saving space:

cpp
struct Flags { unsigned a : 1; unsigned b : 1; unsigned c : 6; };  // 8 bits total

The upside: small sizeof (compact), cache-friendly. The cost: bit operations — reading and writing a bitfield member is "read the whole byte plus bitmask plus bit operation", a few more instructions than reading and writing a plain int. So bitfields save memory, spend instructions. They fit "lots of flag bits, memory is the bottleneck" (protocol headers, flag sets); they don't fit "a single field read/written at high frequency, compute is the bottleneck". Agner vol1 §7.27 has the detailed tradeoffs.

3. enum class: zero overhead

enum class (the C++11 strongly-typed enum) is "type-safe enum", and zero overhead: underneath it's just an int (or whatever underlying type you specify), as fast to access as a plain int, and the type safety is at compile time, zero cost at runtime. So:

  • Prefer enum class over bare int constants (type safety, readability, free).
  • Don't worry about its performance; it's the same as int.
  • Specifying the underlying type (enum class Color : uint8_t) controls sizeof and saves space.

This is one of the few cases where "zero-cost abstraction" actually holds (an exception to Carruth's thesis: not every abstraction has a cost; enum class/optional on the normal path is nearly zero-cost).

sizeof cheat sheet (measured on this machine, libstdc++ C++20)

text
sizeof:
  int                              = 4
  std::optional<int>               = 8   (int 4B + has-value flag + padding)
  std::variant<int,double>         = 16  (double 8B + index + padding)
  std::variant<int,char,double,str>= 40  (string 32B + index + padding)
  std::span<int>                   = 16  (pointer + length, zero ownership)
  std::string_view                 = 16  (pointer + length, no \0 guarantee)
  std::shared_ptr<int>             = 16  (2 pointers: object + control block)
  std::unique_ptr<int>             = 8   (1 pointer)
  std::string                      = 32  (includes SSO buffer)
  std::vector<int>                 = 24  (3 pointers)

How to read it: the extra bytes in optional/variant are the "has-value" flag and the index; span/string_view are lightweight "pointer plus length" views (zero ownership, nearly free); string's 32 bytes include the SSO small buffer (the SSO mechanism is covered in vol3).

How to use this table: when coding, prefer zero- or near-zero-cost abstractions (enum class, span/string_view, optional/variant on the normal path); they make code safer and cost almost nothing in performance. What really deserves attention is virtual function calls (via pointer, not devirtualized), std::function with repeated construction plus a big capture, and exceptions entering a hot loop; these have real cost and often need manual optimization. Always measure before optimizing: an abstraction that "sounds expensive" may already be eliminated by the compiler, and one that "sounds free" (constructing a std::function) may be hiding a heap allocation.

The next article is ch06's last, on RVO/NRVO and move. It isn't really "abstraction cost", it's "the mechanism for returning large objects under value semantics", and it's often misunderstood.

References

  • Agner Fog, Optimizing software in C++ §7 Variables / objects / containers (variable storage types, bitfields, enum) (local)
  • Carruth, There Are No Zero-Cost Abstractions (CppCon 2019) — the "no zero-cost abstractions" thesis
  • ch06-01/02/03/05 (this volume; the measurement source for each cost)
  • The sizeof program for this article: code/volumn_codes/vol6-performance/ch06/abstraction_sizeof.cpp

v0.7.1-2-g3718060 · 3718060 · 2026-07-06