Skip to main content

Initialization Subtleties

advanced10 min readLesson 133 of 204

Copy-init vs direct-init vs list-init, the most-vexing-parse, brace narrowing, and default member initializers โ€” small syntax differences with real consequences.

C++ has ~17 forms of initialization grouped into a few families. Four matter daily:

Direct vs copy vs list

std::string a("x");     // direct-initialization
std::string b = "x";    // copy-initialization: explicit constructors are OFF limits
std::string c{"x"};     // direct-list-initialization
std::string d = {"x"};  // copy-list-initialization

Copy-initialization cannot call an explicit constructor โ€” that keyword is how types opt out of surprising implicit conversions. The flip side matters for overload design: a greedy forwarding constructor only hijacks direct initialization (Tracked c{b};), never copy initialization (Tracked c = b;).

The most vexing parse

Timer t(Log()); declares a function t returning Timer, taking a function returning Log. Braces disambiguate: Timer t(Log{}); constructs.

Narrowing in braces

Braces forbid narrowing conversions โ€” the safe default:

int x{3.9};     // error: narrowing
int y = 3.9;    // compiles: y == 3, silently

Default member initializers and initialization order

Members initialize in declaration order, not initializer-list order โ€” -Wreorder catches the mismatch. Default member initializers centralize defaults while constructors can still override:

struct Session {
    std::chrono::steady_clock::time_point start{std::chrono::steady_clock::now()};
    int retries{0};
    explicit Session(int r) : retries{r} {}   // declaration order: start first
};

Grading note: the checks in this module use brace-initialization traps and static_assert on constructed types. Predict first, then let the compiler grade you.

Now practice

Initialization TrapsBrace narrowing and the most vexing parse โ€” the two initialization traps that reach production code.2 challenges ยท ยท ~14 min