← Return to field notes
systems / systems failure file

Your program can fail before main

A compact C++ failure mode: global initialization crosses translation units, and startup becomes link-order roulette.

reference maintained created 2026-06-23 updated 2026-07-18 4 min 3 sections 0 figures
subscribe via RSS report a correction sec intro ~4 min left read 0%
opening contract reference · maintained
problem
Dynamic initialization across C++ translation units can make startup depend on unspecified ordering before main begins.
scope
The cross-translation-unit initialization-order failure and the construct-on-first-use mitigation.
environment
C++ programs with namespace-scope objects and dynamic initialization in more than one translation unit.

Assumptions

  • At least one global constructor depends on another translation unit.

Limitations

  • This compact reference does not cover every storage-duration, destruction-order, module, or constant-initialization rule.
Table of contents 3 sections
  1. 1 Distrust invisible startup work
  2. 2 Name the cross-unit failure mode
  3. 3 Construct on first use

Distrust invisible startup work

Invisible startup work deserves distrust. Objects with static storage duration in different translation units are initialized in an order the program does not control.

Constant initialization is safe when the compiler can complete it before dynamic startup, but a constructor that reads another translation unit crosses into an ordering regime the source file does not reveal. The dependency graph exists at runtime while the type system and build graph remain silent. Link order, archive extraction, build flags, and refactoring can change the observed sequence without changing either constructor.

Name the cross-unit failure mode

When one global depends on another global from a different translation unit, construction can run too early and fail before main has a chance to establish the system.

The symptom is often misleading: an empty registry, a default-initialized mutex wrapper, a logger that drops the first message, or a crash inside allocator and runtime code. The reproduction may disappear under a different linker or test binary because the implementation changed object ordering, not because the bug was fixed. Destruction has the mirror-image risk when one global uses another after its lifetime ended.

Construct on first use

The practical move is simple: put the dependency behind a function-local static. Construct on first use. Make lifetime explicit. Never let the linker decide your boot sequence.

Since C++11, initialization of a function-local static is synchronized. That makes the first-use boundary explicit and testable, but it does not absolve the design from cycle analysis or teardown policy. For infrastructure registries and process-lifetime services, an intentionally leaked immutable instance can be safer than an elaborate destruction graph. For everything else, prefer explicit ownership passed from main so startup and shutdown order appear in ordinary control flow.

evidence ledger 1 claim
  1. sourced
    C++ initialization rules

    The language reference documents indeterminately sequenced and unspecified initialization cases across translation units.

    inspect source ↗
linked artifacts 1 attached
  • reference
    cppreference: initialization

    Primary public reference for static and dynamic initialization ordering.

    open ↗