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.
Term | Description |
---|---|
Copy Initialization | T x = value; — Can use implicit conversions |
Direct Initialization | T x(value); — Calls constructor directly |
Copy Assignment | x = 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:
- Direct list initialization:
T obj{args...}
(no=
) - 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:
std::initializer_list
constructor (if available and matches)- Regular constructor matching (overload resolution)
- Aggregate initialization (for aggregate types)
- 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
- Prefer list initialization for its safety benefits (prevents narrowing)
- Use direct list initialization (
{}
) over copy list initialization (= {}
) when possible - Be aware of constructor precedence with
std::initializer_list
- Use designated initialization for aggregate types with many fields
- 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.