20 topics
← Back to Quick Reference/
Topic 01
PPrrooggrraamm SSttrruuccttuurree
Translation Units · Linkage · ODR · C++17
C++17 · Advanced ReferenceTranslation Units & Compilation Phases
01What is a Translation Unit?
Each .cpp file is one Translation Unit (TU). The compiler processes TUs independently — it never sees two .cpp files at the same time. Headers are not compiled separately; phase 4 pastes them inline into the TU.
8 Phases of Translation · ISO C++ §5.2
- 1.Character mapping — universal-character-names (
\uXXXX) - 2.Line splicing — backslash + newline merges into one logical line
- 3.Tokenization — source text becomes preprocessing tokens
- 4.
#include, macros,#if/#endif— preprocessor runs - 5.Character-set conversion — encoding of string and char literals resolved
- 6.String literal concatenation —
"a" "b"→"ab" - 7.Translation — compilation proper, produces an object file
- 8.Linking — external references resolved, executable produced
The compiler never sees a header file directly — only the TU after phase 4 has inlined it. This is why redefining a symbol in a header breaks multiple TUs.
Linkage: External · Internal · None
02// ── External linkage — visible to ALL translation units ── int globalVar = 42; // external by default void globalFunc() {} // external by default extern int declaredElsewhere; // declaration (no storage) // ── Internal linkage — THIS translation unit only ────── static int fileLocal = 10; // C-style; works but avoid namespace { // preferred modern idiom constexpr int MAGIC = 0xDEAD; // invisible to other TUs void privateHelper() {} } // ── No linkage — purely local ────────────────────────── void foo() { int x = 5; // no linkage; no address across TUs typedef int T; // no linkage class Local {}; // no linkage (local class) } // Rule of thumb: // Free vars/funcs in a .cpp → prefer anonymous namespace // over 'static' (static has other meanings in class scope)
| External | Default for free functions and global vars — visible to all TUs |
| Internal | Anonymous namespace or static (file-scope) — this TU only |
| No linkage | Local variables, typedefs, local classes — purely local |
One Definition Rule (ODR)
03// One Definition Rule: each entity defined EXACTLY once // across the entire program (with exceptions). // ❌ VIOLATION — variable definition in a shared header // header.h: int counter = 0; // two TUs include → duplicate symbol // ✅ FIX 1: constexpr (implies inline since C++17) constexpr int MAX = 100; // no ODR violation in headers // ✅ FIX 2: inline variable (C++17) inline int counter = 0; // single logical def, safe in headers // ✅ FIX 3: extern + one definition // header.h: extern int counter; // one .cpp: int counter = 0; // ODR-use: "using" a variable in a context that requires its address constexpr int N = 5; int arr[N]; // NOT ODR-used (value only, no address needed) const int* p = &N; // ODR-used → a definition of N is required // Functions: templates and inline functions may be defined // in multiple TUs, but all definitions must be IDENTICAL.
ODR exceptions:
inline functions/variables, templates, and constexpr can be defined in multiple TUs as long as all definitions are identical. The linker picks one.Inline Variables (C++17)
04// ── C++17: inline static data members ────────────────── // Pre-C++17: out-of-line definition required in exactly one .cpp // ❌ Pre-C++17 — definition must live in impl.cpp struct Config { static int counter; // declaration in header }; // impl.cpp: int Config::counter = 0; (required) // ✅ C++17 — definition lives in the header struct Config { inline static int counter = 0; // definition static inline constexpr int MAX = 256; // common idiom inline static const std::string name = "cfg"; }; // Namespace scope (header-only libraries) inline int g_requestCount = 0; // ODR-safe across TUs // constexpr variables are IMPLICITLY inline since C++17 constexpr double TAU = 6.28318530718; // safe in any header // Important: inline means "may appear in multiple TUs" — // it does NOT mean "always inlined by the optimizer".
Header-only libs:
inline variables made true header-only libraries practical in C++17 — no more mandatory .cpp file just for static member definitions.Namespaces
05// ── C++17: nested namespace shorthand ───────────────── namespace App::Core::Util { void helper() {} } // Equivalent pre-C++17: // namespace App { namespace Core { namespace Util { ... }}} // ── Anonymous namespace → internal linkage ───────────── namespace { int magic = 0xDEAD; // invisible outside this TU void localImpl() {} } // ── Inline namespace (ABI versioning) ────────────────── namespace api { inline namespace v2 { // default version struct Result { int code; }; // api::Result → v2::Result } namespace v1 { struct Result { bool ok; }; // api::v1::Result (legacy) } } api::Result r{0}; // resolves to api::v2::Result // ── using-declaration vs using-directive ─────────────── using std::cout; // precise: imports one name using namespace std; // broad: avoid in headers! // ── Namespace aliases ─────────────────────────────────── namespace fs = std::filesystem; // less typing
| namespace A::B::C | C++17 shorthand for triply-nested namespaces |
| namespace { } | Anonymous — gives internal linkage to all enclosed names |
| inline namespace | Enables ADL through parent; used for ABI versioning |
| using std::X | Import one name — safe even in headers (with care) |
| using namespace | Import all — never in headers, pollutes all includers |
if constexpr (C++17)
06#include <type_traits> template<typename T> std::string describe(T val) { if constexpr (std::is_integral_v<T>) { // Only compiled when T is an integer type return "int:" + std::to_string(val); } else if constexpr (std::is_floating_point_v<T>) { return "float:" + std::to_string(val); } else if constexpr (std::is_same_v<T, std::string>) { return "str:" + val; } else { // Parsed but NOT instantiated — static_assert OK here static_assert(always_false<T>, "Unsupported type"); } } // Helper to defer static_assert evaluation template<typename> inline constexpr bool always_false = false; // ── Why it matters ────────────────────────────────────── // Regular if: BOTH branches compile for every T (often fails) // if constexpr: only the matching branch is instantiated // std::is_integral_v<T> is shorthand for // std::is_integral<T>::value (C++17 variable template)
Template specialization lite:
if constexpreliminates the need for many partial template specializations and SFINAE tricks. The discarded branch is parsed but not instantiated — syntax must be valid but type errors don't fire.Structured Bindings (C++17)
07// Structured bindings (C++17) decompose any aggregate/tuple-like // std::pair and std::tuple auto [key, val] = std::make_pair("pi", 3.14159); auto [x, y, z] = std::make_tuple(1, 2.0f, "hi"); // Aggregate struct (binds in declaration order) struct Pixel { uint8_t r, g, b, a; }; auto [r, g, b, a] = Pixel{255, 128, 0, 255}; // C-style array int rgb[3] = {255, 0, 128}; auto& [red, green, blue] = rgb; // & → modifies original // Map iteration — most common real-world use std::map<std::string, int> freq; for (auto& [word, count] : freq) { count++; // modifies map in-place } // const binding (both bindings are const) const auto [lo, hi] = std::make_pair(0, 100); // Bindings to bit-fields NOT allowed (can't take address) // Customization: implement get<N>(), tuple_size, tuple_element
Customization point: Any type can support structured bindings by specializing
std::tuple_size, std::tuple_element, and providing a get<N>() overload — the same protocol used by std::tuple.C++17 Attributes
08// ── [[nodiscard]] ────────────────────────────────────── [[nodiscard]] int openFile(const char* path); openFile("x.txt"); // ⚠ warning: ignoring return value // With message (C++20, widely supported in C++17 compilers) [[nodiscard("check the error code")]] int writeData(); // Applied to a type: every function returning it is nodiscard struct [[nodiscard]] Error { int code; }; // ── [[maybe_unused]] ─────────────────────────────────── [[maybe_unused]] static void debugDump(int v) {} // no warning void setup([[maybe_unused]] int flags) {} // suppress param warn // ── [[deprecated]] ───────────────────────────────────── [[deprecated("use newApi() instead — see docs")]] void legacyApi() {} [[deprecated]] class OldConfig {}; // on types too // ── [[fallthrough]] ──────────────────────────────────── switch (state) { case INIT: setup(); [[fallthrough]]; // explicit intent → no Wimplicit-fallthrough case RUNNING: tick(); break; } // ── [[likely]] / [[unlikely]] (C++20) ───────────────── if ([[likely]] (ptr != nullptr)) { /* fast path */ }
| [[nodiscard]] | Warn when caller discards the return value. Apply to error codes, resource handles, expensive computations. |
| [[maybe_unused]] | Suppress -Wunused-* for variables, parameters, or functions that are intentionally unused in some build configs. |
| [[deprecated]] | Emit a compiler diagnostic at call sites. Provide migration guidance in the message string. |
| [[fallthrough]] | Silences -Wimplicit-fallthrough inside switch statements. Must appear as a statement on its own line. |
Static Initialization Order Fiasco
09// Static Initialization Order Fiasco (SIOF): // Globals in different TUs may initialize in any order. // file a.cpp int Registry::count = 0; // initialized… when? // file b.cpp (may run before a.cpp) extern int Registry::count; int Derived::extra = Registry::count + 1; // ⚠ possibly 0 // ── Solution 1: Meyers Singleton (function-local static) ── int& getCount() { static int count = 0; // initialized on first call (C++11: thread-safe) return count; } int& getRegistry() { static Registry r; // r init guaranteed before first use return r; } // ── Solution 2: constexpr globals (zero-init phase) ─── constexpr int MAX_THREADS = 16; // constant initialization — before any // dynamic init, safe everywhere // ── Solution 3: constinit (C++20) ───────────────────── // constinit int limit = computeLimit(); // error if not constexpr
Rule of thumb: Never let a global variable depend on another global defined in a different
.cpp file. Use function-local statics (Meyers Singleton) or constexpr to guarantee initialization order.main() Contract
10// All valid main() signatures per ISO C++ int main() {} int main(int argc, char* argv[]) {} int main(int argc, char** argv) {} // equivalent // argc — count of arguments INCLUDING program name (≥1) // argv[0] — program name/path (implementation-defined) // argv[argc] == nullptr (guaranteed by the standard) // argv[1..argc-1] — command-line arguments // Return value #include <cstdlib> return EXIT_SUCCESS; // expands to 0 return EXIT_FAILURE; // expands to 1 (non-zero) return 42; // any non-zero = failure by convention // Implicit return 0 (C++11, ONLY for main) int main() { } // well-defined: equivalent to return 0; // Cleanup hooks std::atexit([]{ flushLogs(); }); // runs on normal exit std::at_quick_exit([]{ /* ... */ }); // runs on std::quick_exit() // Environment (implementation-defined) #include <cstdlib> if (const char* p = std::getenv("HOME")) { /* use p */ }
main() is special: It is the only function with an implicit
return 0, the only function where int can be the return type without a return statement, and the only user-defined function that may not be called or its address taken.const vs constexpr
11// const — immutable after init; may be runtime value const int a = rand(); // runtime const — OK const int b = 42; // compile-time eligible // constexpr — MUST be evaluatable at compile time constexpr int MAX = 1024; constexpr int square(int x) { return x * x; } // C++11 constexpr int S = square(5); // 25, computed at compile time int n = square(rand()); // computed at runtime (non-constexpr arg) // C++17 constexpr improvements // ✅ constexpr lambdas auto clamp = [](auto v, auto lo, auto hi) constexpr { return v < lo ? lo : (v > hi ? hi : v); }; // ✅ constexpr if (see if constexpr card) // ✅ constexpr variables are implicitly inline at namespace scope // Key distinctions // const: immutable reference; address CAN be taken // constexpr: guaranteed compile-time; address can still be taken // inline: ODR exception; safe in multiple TUs // constexpr implies const (for variables) // constexpr implies inline (for namespace-scope variables, C++17)
| const | Value won't change after init. May be runtime (e.g. const int n = argc). Address can be taken. |
| constexpr | Must be evaluatable at compile time. Implies const. Enables use in template args, array sizes, case labels. |
| constexpr fn | C++17 relaxed rules: can contain loops, local vars, if/switch. Evaluated at compile time if all args are constexpr. |
| C++17 bonus | constexpr implies inline for namespace-scope variables — safe to define in headers without ODR violation. |
std::byte (C++17)
12#include <cstddef> // std::byte // std::byte: opaque representation of a raw byte // NO arithmetic — only bitwise operations defined std::byte b{0xFF}; std::byte mask{0x0F}; std::byte r1 = b & mask; // AND → 0x0F std::byte r2 = b | mask; // OR → 0xFF std::byte r3 = b ^ mask; // XOR → 0xF0 std::byte r4 = ~b; // NOT → 0x00 std::byte r5 = b << 2; // left shift std::byte r6 = b >> 1; // right shift // Explicit conversions required int val = std::to_integer<int>(b); // → 255 std::byte c = static_cast<std::byte>(42); // Why not char or uint8_t? // char: may be signed; aliasing rules differ // uint8_t: arithmetic operators defined — accidents compile silently // e.g., uint8_t x = 200; x + 100 overflows silently // std::byte: b + 1 is a COMPILE ERROR → bugs surface early
Use when: working with raw buffers, serialization, memory-mapped I/O, or network packets.
std::byte communicates intent and prevents accidental arithmetic on raw bytes.Compiler Flags & Sanitizers
13# ── C++17 strict compilation ───────────────────────── g++ -std=c++17 -Wall -Wextra -Wpedantic -Werror # -Wall core warnings (unused vars, shadowing, …) # -Wextra extra diagnostics on top of -Wall # -Wpedantic reject non-conforming extensions # -Werror treat all warnings as errors # ── Optimization levels ─────────────────────────────── -O0 # no optimization — fastest compile, best debug -O1 # basic (constant folding, inlining, …) -O2 # moderate — standard for release builds -O3 # aggressive — may increase binary size -Os # minimize binary size (O2 minus size-increasing opts) -Og # debug-friendly optimization (GCC; better than -O0) # ── Debug symbols ───────────────────────────────────── -g # standard DWARF info (gdb/lldb) -ggdb # GDB-specific extensions (richer backtraces) # ── Runtime sanitizers (use with -O1 or lower) ──────── -fsanitize=address # buffer OOB, use-after-free, leaks -fsanitize=undefined # signed overflow, null deref, misalign -fsanitize=thread # data races -fsanitize=memory # uninitialized reads (Clang only) # ── Recommended dev build ───────────────────────────── g++ -std=c++17 -O1 -g -fsanitize=address,undefined \ -Wall -Wextra -Werror main.cpp -o app
Sanitizer overhead: AddressSanitizer adds ~2× runtime overhead; UBSanitizer adds ~1.5×. Use in CI and during development. Never ship with sanitizers enabled — they require the sanitizer runtime to be present.