Skip to content

C++ abstraction costs: a cheat sheet ​

This article is ch06's reference card: it rolls the C++ abstraction costs measured in the earlier articles into a single cheat sheet, then adds three short entries that never got an article of their own (variable storage types, bitfields, enum class). Next time you're coding and wonder "is this abstraction expensive?", look it up here.

What we measured earlier: the cost cheat sheet ​

AbstractionMain costMeasured numbers (this machine)When to care
Virtual function (via pointer)vtable lookup + indirect jump + blocks inlining0.55 ns, 2.5× 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 a concern
Exception (throwing path)EH table lookup + stack unwinding857 ns, ~3400×Keep the exception path out of hot loops
std::function callType-erased indirect call1.61 ns, 6× a direct lambdaA million calls per frame
std::function constructionSmall capture takes SBO, big one heap-allocatesSBO 2.3 ns / heap 19.6 ns (8.5×)Repeated construction + big capture on the hot path
RVO/NRVOReturn value constructed directly in the caller0 copies, 0 movesDon't write std::move when returning 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 the mechanism and the experiment. The overarching thesis (Carruth, No Zero-Cost Abstractions): every C++ abstraction maps to a hardware cost, but "has a cost" doesn't mean "pays 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 vol 1 §7.1):

  • Automatic variables (stack): the default. Fastest to access (a stack that hits in L1), and the compiler can put them in registers. The register keyword is meaningless on modern compilers (they allocate registers themselves); it was deprecated in C++11 and removed in 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). Under multithreading, initialization of a static local variable is thread-safe (magic statics), but thread-safe initialization has a runtime cost (an atomic check on first entry).
  • thread_local: one copy per thread. Access is slightly more expensive (it has to look up the thread-local storage area through TLS, usually a few extra instructions), but it avoids sharing under multithreading. Useful for "per-thread context objects".

In practice: keep hot-path variables automatic where possible (let the compiler put them in registers); static global constants are free; use thread_local for per-thread context (and count its initialization and destruction cost into the thread's lifecycle).

2. Bitfields ​

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

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

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

3. enum class: zero overhead ​

enum class (the C++11 strongly-typed enum) is an enum "with type safety attached", and it's 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 compile-time — zero runtime cost. So:

  • Prefer enum class over bare int constants (type safety and readability, for 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" genuinely holds (an exception to Carruth's thesis: not every abstraction has a cost — enum class/optional on the normal path is near zero cost).

sizeof quick reference (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 a value" flag and the index; span/string_view are lightweight "pointer + length" views (zero ownership, nearly free); and string's 32 bytes include the SSO small buffer (the SSO mechanism belongs to vol3).

How to use this table: when writing code, reach first for the zero- or near-zero-cost abstractions (enum class, span/string_view, optional/variant on the normal path) — they make the code safer and cost almost nothing in performance. What actually deserves your attention is the short list of virtual calls (via pointer, not devirtualized), std::function repeatedly constructed with a big capture, and exceptions entering a hot loop: those have real cost and often need manual optimization. And always measure before optimizing — an abstraction that "sounds expensive" may have been eliminated by the compiler long ago, while 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 an "abstraction cost" so much as 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 copy
  • Carruth, There Are No Zero-Cost Abstractions (CppCon 2019) — the "no zero-cost abstractions" thesis
  • ch06-01/02/03/05 (this volume; where each cost was measured)
  • The sizeof program for this article: code/volumn_codes/vol6-performance/ch06/abstraction_sizeof.cpp

pdf-latest-4-g85128cc · 85128cc · 2026-10-05