Modern C++ Initialization

C++ offers more ways to initialize a variable than almost any other mainstream language — assignment, direct, copy, and brace initialization all exist side by side, each with its own rules about narrowing, constructor selection, and edge cases.

int x = 5; int y(5); int z{5}; int w = {5}; — all four of these initialize an int to 5, and all four behave identically for a simple built-in type. The moment types get more interesting (classes with constructors, containers, aggregates) those four forms stop being interchangeable, and picking the wrong one silently changes which constructor runs, whether a dangerous narrowing conversion is allowed, or whether the compiler parses your line as a variable declaration at all. This page works through the historical initialization forms, why brace (uniform) initialization was introduced in C++11 to unify them, how std::initializer_list constructors interact with braces in ways that can be surprising, narrowing prevention, aggregate initialization, and designated initializers from C++20 — everything you need to choose the right initialization syntax deliberately instead of by habit.

Copy Initialization vs Direct Initialization

Copy initialization uses an = sign at the point of declaration: std::string s = "hi";. Despite the name and the = sign, this does not necessarily involve an actual copy at runtime — the compiler is permitted (and, since C++17, required) to construct s directly from the string literal, skipping any intermediate temporary. Direct initialization drops the = and uses parentheses instead: std::string s("hi");, calling a matching constructor directly. For simple cases the two forms produce identical results; the distinction matters mainly for constructors marked explicit and for a parsing ambiguity covered later on this page.

explicit specifically blocks the implicit-conversion form of copy initialization (Widget w2 = 42;, which would otherwise implicitly convert 42 to a Widget), while leaving direct initialization and explicit construction untouched. This is exactly why single-argument constructors that shouldn't double as implicit conversions are marked explicit.

#include <string>

class Widget
{
public:
    explicit Widget(int id) : id_(id) {}
private:
    int id_;
};

Widget w1(42);       // direct-initialization: OK, calls Widget(int) directly
Widget w2 = 42;      // copy-initialization: ERROR -- explicit blocks the implicit conversion path
Widget w3 = Widget(42); // OK -- Widget(42) is itself a direct call; the outer = just binds it

Assignment Is Not Initialization

int x = 5; is initialization -- x doesn't exist as an object with an indeterminate value at any point before it becomes 5. int x; x = 5; is two separate steps: x is default-initialized first (left with an indeterminate value for a built-in type, or default-constructed for a class type), and only then does a genuinely separate assignment operation overwrite it. The distinction matters because a type might have an expensive or even deleted default constructor, and because reading x between those two lines (in the built-in case) is undefined behavior.

A type with no default constructor simply cannot be default-initialized and then assigned later -- it must be given its value at the point of construction. This is one reason to prefer initializing members in a constructor's initializer list rather than assigning to them in the constructor body.

class NoDefault
{
public:
    NoDefault(int v) : v_(v) {}
private:
    int v_;
};

// NoDefault n;       // ERROR: no default constructor to call
NoDefault n(10);      // OK: initialized directly with a value from the start
// n = NoDefault(20); // this would be a separate assignment, requiring operator= to exist too

Brace (List) Initialization and Its Uniform Syntax

C++11 introduced brace initialization, sometimes called uniform initialization because the same {} syntax works for scalars, classes, aggregates, and containers alike: int x{5}; std::vector<int> v{1, 2, 3}; std::string s{"hi"}; Widget w{42};. Under the hood, braces route through list-initialization, which first looks for a constructor taking a std::initializer_list before considering ordinary constructors -- a rule that has real, sometimes surprising, consequences covered in the next section.

v ends up holding {1, 2, 3} (three elements matching the literal values). v2, deliberately using parentheses instead of braces, ends up holding {7, 7, 7} -- three copies of 7 -- because parentheses skip the initializer_list constructor entirely and match std::vector's (size_t count, const T& value) constructor instead. Mixing up braces and parentheses on std::vector is one of the most common sources of confusion in modern C++ code.

int a{5};                 // direct-list-initialization
int b = {5};               // copy-list-initialization
std::vector<int> v{1,2,3}; // calls the initializer_list constructor: {1,2,3}
std::vector<int> v2(3, 7); // calls the (count, value) constructor: three 7s -- NOT braces!

std::initializer_list and Constructor Selection

When a class has both an std::initializer_list constructor and other constructors, brace initialization strongly prefers the initializer_list overload whenever the braced arguments can plausibly form one -- even if another constructor would otherwise be a better or more natural match. This preference is by design (it's what makes std::vector<int> v{1,2,3}; work) but it means adding an initializer_list constructor to a class can silently change what existing brace-initialization call sites do.

c1 and c2 pass the exact same two integers, but only the parenthesized call reaches the (int, int) constructor. The braced call matches the initializer_list<int> constructor instead, because list-initialization checks for a matching initializer_list constructor first and, finding one that fits, never even considers the other overload.

#include <vector>
#include <iostream>

class Container
{
public:
    Container(int count, int value) { std::cout << "count/value ctor\n"; }
    Container(std::initializer_list<int> list) { std::cout << "initializer_list ctor\n"; }
};

int main()
{
    Container c1(2, 5);   // parentheses: count/value ctor -- prints "count/value ctor"
    Container c2{2, 5};   // braces: initializer_list ctor wins -- prints "initializer_list ctor"
    return 0;
}

Narrowing Prevention

One of brace initialization's genuine safety improvements is that it forbids narrowing conversions -- implicit conversions that could lose information, such as double to int or a large int to a smaller int type -- at compile time. The equivalent parenthesized or assignment forms allow these conversions silently, which has historically been a source of subtle bugs.

Only the braced form c{3.14}; is rejected by the compiler. This is a compile-time guarantee, not a runtime check, which is exactly why brace initialization is recommended as a default wherever it's practical -- it turns an entire class of silent truncation bugs into build errors.

int a = 3.14;   // OK (but silently truncates to 3) -- assignment allows narrowing
int b(3.14);    // OK (also truncates to 3) -- direct-init allows narrowing too
int c{3.14};    // ERROR: narrowing conversion from double to int inside { }

int good{3};    // OK -- no narrowing, 3 fits exactly into int with no loss

The Most Vexing Parse

Parenthesized direct-initialization has a famous parsing trap known as the most vexing parse: a line that looks like it default-constructs an object with empty parentheses can instead be parsed as a function declaration, because C++'s grammar allows that reading and the standard requires the compiler to prefer it whenever both are possible.

Because Timer t1(); is syntactically valid as a function declaration, the compiler chooses that interpretation, and any later code that tries to use t1 as an object (t1.someMethod();) fails to compile with a confusing error about t1 not being a class type. Brace initialization, Timer t2{};, has no equivalent ambiguity and is one of the reasons uniform initialization was introduced.

#include <string>

class Timer {};

Timer t1();        // NOT a Timer object! This declares a function t1 taking no
                   // arguments and returning a Timer -- the classic vexing parse.

Timer t2{};        // Actually default-constructs a Timer object -- braces have no such ambiguity
Timer t3;          // Also actually default-constructs a Timer object

Aggregate Initialization

An aggregate is, roughly, a class or struct with no user-declared constructors, no private or protected non-static data members, no virtual functions, and no base classes (with looser rules for inheritance added in later standards). Aggregates can be initialized member-by-member with a braced list, in declaration order, without any constructor being involved at all.

Because Point has no constructors of its own, {10, 20} initializes its members positionally rather than calling any function. Members with no corresponding braced value (as with p2's y) are value-initialized -- zero for a built-in numeric type.

struct Point
{
    int x;
    int y;
};

Point p1{10, 20};      // x = 10, y = 20
Point p2{10};          // x = 10, y = 0 (value-initialized: missing members are zero-initialized)
Point p3{};            // x = 0, y = 0

Designated Initializers (C++20)

C++20 added designated initializers, letting you name which aggregate member each value goes to, rather than relying purely on declaration order -- a feature C had for decades before C++ adopted a restricted version of it. C++'s version requires the named members to appear in declaration order and does not allow skipping around arbitrarily.

Naming each member makes the call self-documenting -- readers don't have to cross-reference Config's member order to know which number means what. cfg2 illustrates that skipped members (height here) still get value-initialized, exactly as with plain positional aggregate initialization.

struct Config
{
    int width;
    int height;
    bool fullscreen;
};

Config cfg{ .width = 1920, .height = 1080, .fullscreen = true };
Config cfg2{ .width = 800, .fullscreen = false }; // height is value-initialized to 0

Choosing an Initialization Style

A practical default for new code: prefer brace initialization for scalars and most class types, both for its narrowing protection and to sidestep the most vexing parse -- with the specific exception of std::vector (and similarly designed types) when you actually want the (count, value) constructor, where parentheses are required to avoid triggering the initializer_list overload instead. Use aggregate initialization for plain data-holding structs, and reach for designated initializers in C++20 code when a struct has enough fields that positional initialization would be hard to read correctly at a glance.

Why does adding a std::initializer_list constructor to a class potentially break existing code that constructs it with braces?

List-initialization prefers a matching initializer_list constructor over any other constructor whenever the braced arguments can form one. If a class previously had, say, a (int, int) constructor and existing code called it with braces, adding an initializer_list<int> constructor causes those same braced calls to silently start resolving to the new constructor instead, changing behavior without any change at the call site.

What is the most vexing parse, and how does brace initialization avoid it?

It's the rule that a statement like Timer t(); is parsed as a function declaration (a function named t taking no parameters and returning Timer) rather than as default-constructing an object, because the grammar allows that reading and the compiler is required to prefer it. Timer t{}; has no equivalent ambiguity in the grammar, so it's unambiguously an object definition.

Why does int c{3.14}; fail to compile while int b(3.14); and int a = 3.14; both compile (with silent truncation)?

Brace initialization specifically disallows narrowing conversions -- ones that could lose information, like double to int -- as a compile-time error. Direct initialization with parentheses and copy initialization with = both permit narrowing conversions to happen implicitly and silently, which is exactly the class of bug narrowing prevention in braces was designed to catch.

What makes a class or struct an aggregate, and what does that mean for how it's initialized?

Roughly: no user-declared constructors, no private or protected non-static data members, no virtual functions, and no base classes. Aggregates can be initialized directly with a braced list of member values in declaration order (or, in C++20, with designated initializers naming each member) without any constructor call being involved at all -- the members are initialized individually, positionally.

In struct Point { int x; int y; }; Point p{10};, what value does p.y end up with, and why?

p.y is value-initialized to 0. Aggregate initialization assigns braced values to members in declaration order; any trailing member with no corresponding value in the braced list -- here, y, since only one value (10) was supplied for x -- is value-initialized, which zeroes out a built-in numeric type.

What does this program print, and why do v1 and v2 end up with different sizes despite looking so similar?

It prints 4 2. v1(4, 9) uses parentheses, calling std::vector's (count, value) constructor -- four elements, each set to 9. v2{4, 9} uses braces, which prefer the initializer_list constructor, producing a two-element vector holding the values 4 and 9 directly.

Write a small class Rectangle with an explicit two-argument constructor Rectangle(int width, int height), and demonstrate in main() one line that legally copy-initializes a Rectangle from two ints using direct-list-initialization braces, plus one commented-out line showing the copy-initialization form that explicit blocks.

Rectangle r1{3, 4}; is direct-list-initialization -- braces attached directly to the variable name, with no = sign -- which explicit does not block, since it's not an implicit conversion path. Rectangle r2 = {3, 4}; is copy-list-initialization (the = sign makes it a copy-initialization context even though braces are used), which explicit does block, because it would require an implicit construction from the braced values to produce the value that gets copied.

Write a C++20 program using a struct ServerConfig with fields host (std::string), port (int), and useTls (bool), constructed with designated initializers that set host and port but deliberately omit useTls, then print all three fields to show useTls defaulted correctly.

ServerConfig is a plain aggregate, so designated initializers can set host and port by name while leaving useTls unmentioned. An omitted member in aggregate initialization is value-initialized, so useTls becomes false (printed as 0) even though it was never explicitly assigned, and the output reads localhost:8080 tls=0.

What are the values of w.id and w.price after this line, and why does this compile even though only one value is given for a two-member struct?

w.id is 7 and w.price is 0.0. Widget is an aggregate, so braced initialization assigns values to members in declaration order; id gets the single supplied value, 7, and price, having no corresponding value in the list, is value-initialized to 0.0. This is legal aggregate initialization, not an error -- aggregates never require every member to be given a value explicitly.

#include <vector>
#include <iostream>

int main()
{
    std::vector<int> v1(4, 9);
    std::vector<int> v2{4, 9};

    std::cout << v1.size() << " " << v2.size() << "\n";
    return 0;
}
struct Widget
{
    int id;
    double price;
};

Widget w{7};