C++ Initialization Order: Static Fiasco, Members, and TU Rules

C++ Initialization Order: Static Fiasco, Members, and TU Rules

이 글의 핵심

Complete guide to initialization order in C++: translation units, member declaration order, static fiasco, and production-safe patterns.

What is initialization order—and why it matters

Problem: Global y might be initialized using global x from another translation unit—cross-TU order is unspecified.

file1.cpp:

int compute() { return 100; }
int x = compute();  // dynamic initialization

file2.cpp:

extern int x;
int y = x * 2;  // x might not be initialized yet!

This is the static initialization order fiasco.

flowchart TD
    subgraph file1["file1.cpp"]
        x["int x = compute()"]
    end
    subgraph file2["file2.cpp"]
        y["int y = x * 2"]
    end
    subgraph order["initialization order"]
        q["which runs first?"]
        a["unspecified across TUs"]
    end
    x --> q
    y --> q
    q --> a

Table of contents

  1. Initialization phases
  2. Order within one TU
  3. Order across TUs
  4. Member initialization order
  5. Static initialization order fiasco
  6. Common mistakes
  7. Production patterns
  8. Full example

1. Initialization phases

Three stages

static int a;           // zero initialization

constexpr int b = 10;   // constant initialization

constinit int c = 20;   // C++20: compile-time check

int func() { return 30; }
int d = func();         // dynamic initialization

// Conceptually: zero -> constant -> dynamic
PhaseWhenExample
ZeroBefore dynamic initstatic int x; → 0
ConstantCompile timeconstexpr int x = 10;
DynamicRuntimeint x = f();

2. Order within one translation unit

Top-to-bottom declaration order for namespace-scope objects in that TU.

int a = 10;
int b = a * 2;  // a first -> b = 20
int c = b + a;  // b,a first -> c = 30

3. Order across translation units

Unspecified for dynamic initialization of namespace-scope objects in different TUs.

file1.cpp — global Logger
file2.cpp — global Database using Logger in its constructor

If Database initializes before Logger, you may use Logger before its constructor runs—UB.


4. Member initialization order

Declaration order wins, not the order in the ctor initializer list.

struct Data {
    int b;
    int a;
    
    Data() : a(10), b(a * 2) {
        // actual order: b, then a (declaration order)
        // b = a*2 runs while a is still uninitialized
    }
};

Fix: declare a before b if b depends on a.

Base → members → ctor body

struct Base {
    Base() { std::cout << "1. Base\n"; }
};

struct Member {
    Member() { std::cout << "2. Member\n"; }
};

struct Derived : Base {
    Member m;
    Derived() {
        std::cout << "3. Derived\n";
    }
};

// Output: 1 Base, 2 Member, 3 Derived

5. Static initialization order fiasco

Problem

// file1
int x = 100;

// file2
extern int x;
int y = x * 2;  // x might still be 0

Fix 1: Meyers singleton (function-local static)

class Logger {
public:
    static Logger& instance() {
        static Logger logger;  // initialized on first use (C++11 thread-safe)
        return logger;
    }
    void log(const char* msg) { /*...*/ }
private:
    Logger() {}
};

class Database {
public:
    Database() {
        Logger::instance().log("Database created");
    }
};

Fix 2: constinit (C++20)

constinit int x = 100;

extern constinit int x;
constinit int y = x * 2;  // both constant-init compatible

Fix 3: Lazy init via function

int& get_x() {
    static int x = 100;
    return x;
}

int& get_y() {
    static int y = get_x() * 2;
    return y;
}

6. Common mistakes

Member order mismatch

Match declaration order and initializer list.

Cross-TU globals

Avoid non-constant dependencies; prefer function access or constinit where applicable.

Static destruction order

Destructors of globals run in reverse order of initialization—avoid touching another global’s state in a destructor unless order is guaranteed (usually you cannot).


7. Production patterns

Meyers singleton

class ResourceManager {
public:
    static ResourceManager& instance() {
        static ResourceManager mgr;
        return mgr;
    }
    void load() {}
private:
    ResourceManager() {}
    ResourceManager(const ResourceManager&) = delete;
    ResourceManager& operator=(const ResourceManager&) = delete;
};

constinit constants

constinit int MAX_CONNECTIONS = 1000;

Nifty counter (legacy I/O streams style)

Sometimes used for libraries that must initialize exactly once across TUs—prefer simpler patterns in new code.


8. Example: safer globals

See the Korean article’s ResourceManager / Database / Cache sample—pattern: first use initializes the manager; globals register during dynamic init but only touch the singleton through instance().


Rules summary

ScopeOrder
Within one TUDeclaration order
Across TUsUnspecified for dynamic init
MembersDeclaration order (not ctor list order)
Base / members / bodyBase → members → constructor body
DestructionReverse of initialization

FAQ

Q1: Cross-TU init order?

A: Unspecified for dynamic initialization—do not depend on it.

Q2: What is the static initialization order fiasco?

A: Globals in different TUs can observe each other half-initialized if you depend on order.

Q3: Fixes?

A: Function-local statics, constinit, lazy initialization.

Q4: Member order?

A: Declaration order in the class—must match dependencies.

Q5: What is constinit?

A: C++20—variable must have static initialization; cannot be initialized by a non-constexpr runtime call.

Q6: Resources?

A: cppreference — initialization, Effective C++ Item 4, C++ Primer.


  • Static members

Practical tips

Debugging

  • Warnings first

Performance

  • Profile

Code review

  • Conventions

Practical checklist

Before coding

  • Right approach?
  • Maintainable?
  • Performance?

While coding

  • Warnings?
  • Edge cases?
  • Errors?

At review

  • Intent?
  • Tests?
  • Docs?

Keywords

C++, initialization order, static initialization, fiasco, constinit


  • Dynamic initialization
  • Static init order error