Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

List Initialization in C++

"Traditional" Initialization Styles

Before list initialization, C++ supported two main initialization styles: copy initialization (=) and direct initialization (()).

struct Widget {
    Widget(int a) {}
};

int main() {
    // Copy initialization
    int a = 10;

    // Direct initialization
    int b(20);
    
    // Copy initialization: creating a new object and initializing it
    // using a constructor that can take 5 as an argument
    Widget w1 = 5;
    
    // Direct initialization
    Widget w2(5);      
}

Copy Initialization

Copy initialization (=) can trigger implicit constructor calls. Using explicit on a constructor disables implicit conversions:

struct Widget {
    explicit Widget(int a) {}
};

Widget w = 10;  // Error: explicit constructor blocks implicit conversion

Important note: In C++, "copy initialization" refers to the syntax, not necessarily the action of copying:

MyClass a = 5;   // Copy initialization syntax

This syntax looks like assignment (=), but it's actually initializing a, not assigning to an existing object. The name "copy initialization" is historical—it suggests "initializing an object using a value", even though modern compilers typically optimize away any actual copying through copy elision.

Copy initialization also occurs in other contexts:

void process(Widget w) {}    // Parameter initialization

Widget create() { 
    return 10;               // Return value initialization
}

int main() {
    process(7);              // Copy initialization of parameter
    Widget w = create();     // Copy initialization of w
}

For primitive types like int, there is no difference under the hood between copy initialization and direct initialization. Both generate identical assembly code.

int a = 10;    // Copy initialization, C-style
int b(20);     // Direct initialization, function call style

Both will typically compile to something like:

mov dword ptr [a], 10
mov dword ptr [b], 20

Direct Initialization

Direct initialization (()) calls constructors explicitly without implicit conversions.

TermDescription
Copy InitializationT x = value; — Can use implicit conversions
Direct InitializationT x(value); — Calls constructor directly
Copy Assignmentx = other; — Modifies an existing object (not initialization)

List Initialization

The C++ Standard defines list initialization as "initialization of an object or reference from a braced-init-list". It's also informally called "brace initialization".

C++11 introduced list initialization using {}:

int a = {42};      // Copy list initialization
int b{42};         // Direct list initialization

struct Widget {
    Widget(std::string, int) {}
    Widget(int) {}
};

Widget w1 = {7};                  // Copy list initialization
Widget w2{7};                     // Direct list initialization
process({7});                     // Copy list initialization (parameter)
process({"hello", 5});            // Copy list initialization (parameter)
Widget w3 = create();             // Copy initialization (return value)
Widget* w4 = new Widget{"hi", 9}; // Direct list initialization

Two Forms of List Initialization

List initialization comes in two forms:

  1. Direct list initialization: T obj{args...} (no =)
  2. Copy list initialization: T obj = {args...} (with =)

The key difference is how they interact with explicit constructors:

#include <string>

struct Widget {
    explicit Widget(std::string, int) {}
    Widget(int) {}
};

int main() {
   // Works: Direct list initialization of temporary + copy initialization
   // Copy elision ensures no actual copying occurs
   auto w1 = Widget{"hello", 5}; 

   // Error: Copy list initialization blocked by explicit constructor
   // The compiler cannot implicitly convert {"hello", 5} to Widget
   Widget w2 = {"hello", 5};    

   // Works: Direct list initialization
   Widget w3{"hello", 5};
}

How List Initialization Works

List initialization has multiple mechanisms with the following precedence:

  1. std::initializer_list constructor (if available and matches)
  2. Regular constructor matching (overload resolution)
  3. Aggregate initialization (for aggregate types)
  4. Value initialization (for empty braces {})

Examples of different mechanisms:

// Arrays and containers
int arr[] = {1, 2, 3};                                // Copy list initialization
std::vector<int> v{1, 2, 3};                          // Uses initializer_list constructor
std::set<int> s{4, 5, 6};                             // Uses initializer_list constructor
std::map<std::string, int> m{{"dog", 1}, {"cat", 2}}; // Uses initializer_list constructor

// Regular constructor matching
Widget w{"hello", 5};                                 // Uses Widget(std::string, int)

Using std::initializer_list

Many standard containers support std::initializer_list constructors:

std::vector<int> v{1, 2, 3, 4, 5};  // Uses initializer_list<int> constructor

You can add std::initializer_list support to custom types:

#include <initializer_list>

struct MyCollection {
    MyCollection(std::initializer_list<std::string> items) {
        for (const auto& item : items) {
            std::cout << item << " ";
        }
    }
};

MyCollection c{"alpha", "beta", "gamma"};  // Uses initializer_list constructor

When you write MyCollection c{"alpha", "beta", "gamma"}, the compiler roughly transforms it to:

// Conceptual transformation (implementation-defined details)
const std::string temp_array[] = {"alpha", "beta", "gamma"};
MyCollection c(std::initializer_list<std::string>(temp_array, temp_array + 3));

Advantages and Pitfalls

Preventing Narrowing Conversions

List initialization disallows implicit narrowing conversions, improving type safety:

int x = 300;
char a1 = x;      // OK with traditional initialization (potentially unsafe)
char a2{x};       // Error: narrowing conversion not allowed

unsigned int u1 = {-1};   // Error: negative to unsigned
int i1 = {2.5};           // Error: float to int
float f1{3};              // OK: int to float is safe
double d = 3.14159;
float f2{d};              // Error: potential precision loss

Narrowing includes:

  • Floating-point to integer conversion
  • Larger to smaller integer types (when value doesn't fit)
  • Integer to floating-point (when not exactly representable)
  • Signed to unsigned (when negative)

Constructor Preference Gotcha

When both regular and initializer_list constructors match, the initializer_list version takes precedence:

std::vector<int> v1(3, 2);    // Regular constructor: 3 elements, each with value 2
std::vector<int> v2{3, 2};    // initializer_list constructor: 2 elements with values 3 and 2

// Be careful with this difference!
std::vector<int> empty1(0);   // Empty vector
std::vector<int> empty2{0};   // Vector with one element: 0

Nested Initialization

For nested types like std::map, list initialization works hierarchically:

std::map<std::string, int> m{{"fox", 1}, {"owl", 2}};
//                            ^^^^^^^^^  ^^^^^^^^^
//                            Each creates a std::pair
//                           ^^^^^^^^^^^^^^^^^^^^^^^^
//                           Outer braces create initializer_list<pair>

Designated Initialization (C++20)

C++20 introduced designated initialization for aggregate types:

struct Point { int x; int y; };
Point p{.x = 4, .y = 2};  // Named field initialization

This is especially useful for structs with many fields:

struct Config {
    int width = 800;
    int height = 600;
    bool fullscreen = false;
    int samples = 4;
};

Config cfg{.width = 1920, .height = 1080, .fullscreen = true};
// Unspecified fields keep their default values

Designated Initialization Rules

Requirements:

  • Only works with aggregate types (no user-declared constructors, virtual functions, etc.)
  • Only non-static data members can be designated
  • Must follow declaration order:
    Point p{.y = 1, .x = 2};  // Error: wrong order
    

Restrictions:

  • Each field can be initialized only once:
    Point p{.x = 1, .x = 2};  // Error: duplicate initialization
    
  • Cannot mix designated and positional initialization:
    Point p{.x = 1, 2};       // Error: mixed styles
    
  • For unions, only one member can be designated:
    union Data { int a; double b; };
    Data d{.a = 10, .b = 3.14};  // Error: multiple members
    
  • No direct nested access (use nested braces instead):
    struct Line { Point start, end; };
    Line l{.start.x = 1};         // Error: nested access
    Line l{.start{.x = 1}};       // OK: nested initialization
    

Best Practices

  1. Prefer list initialization for its safety benefits (prevents narrowing)
  2. Use direct list initialization ({}) over copy list initialization (= {}) when possible
  3. Be aware of constructor precedence with std::initializer_list
  4. Use designated initialization for aggregate types with many fields
  5. Mark single-argument constructors explicit unless implicit conversion is specifically desired

Summary

List initialization is a powerful C++ feature that provides:

  • Uniform syntax for initialization
  • Type safety through narrowing prevention
  • Flexibility through multiple initialization mechanisms
  • Readability improvements, especially with designated initialization (C++20)

Understanding the distinction between direct and copy list initialization, along with their interaction with explicit constructors, is crucial for effective modern C++ programming.