Disclaimer: ChatGPT generated document.
Below is a practical map of C++ initialization types, with syntax, behavior, pros, cons, and typical use cases.
T x;
T* p = new T;Meaning depends on T.
For class types, the default constructor is called:
std::string s; // empty string
std::vector<int> v; // empty vectorFor fundamental local variables, no initialization happens:
int x; // indeterminate value
double d; // indeterminate valueFor objects with static storage duration, zero-initialization happens first:
static int x; // 0Good for class types with meaningful default constructors.
std::mutex m;
std::string name;
std::vector<int> values;Dangerous for primitive local variables.
int x;
std::cout << x; // undefined behaviorUse for types whose default constructor gives a valid state.
Avoid for raw primitive locals unless you will immediately assign them before reading.
T x{};
T x = T();
T x = T{};
new T()
new T{}For fundamental types, this gives zero:
int x{}; // 0
double d{}; // 0.0
bool b{}; // falseFor class types, it calls a constructor, usually the default constructor:
std::string s{}; // empty
std::vector<int> v{}; // emptyFor aggregates, it initializes members recursively:
struct Point {
int x;
int y;
};
Point p{}; // x = 0, y = 0Usually the safest general-purpose initialization form.
int count{};
std::string name{};
Point p{};Prevents uninitialized primitive values.
Can accidentally call std::initializer_list constructors for some class types when braces are involved.
std::vector<int> a(10); // 10 elements, all 0
std::vector<int> b{10}; // 1 element: 10Use as your default choice for local variables:
int retries{};
double total{};
std::string label{};Zero initialization is not usually written directly as a syntax form. It is part of other initialization processes.
Examples:
static int x; // zero-initialized
int y{}; // value-initialized, resulting in zero
int* p{}; // nullptrFor class objects, zero-initialization may happen before default/value initialization in certain cases.
struct S {
int x;
};
S s{}; // s.x == 0Essential for safe initialization of scalars, pointers, and static objects.
You usually do not “choose” zero-initialization directly. It is part of a larger initialization rule.
Prefer brace/value initialization when you want zero:
int x{};
int* p{};
double d{};T x = expr;Examples:
int x = 42;
std::string s = "hello";
std::vector<int> v = {1, 2, 3};Despite the name, copy initialization often does not actually copy. Since modern C++, copy elision and move semantics mean this is often efficient.
std::string make();
std::string s = make(); // usually constructed directlyReadable and familiar.
Good when converting from another type:
std::string s = "hello";
double d = 42;Does not consider explicit constructors.
struct X {
explicit X(int) {}
};
X a(1); // okay
X b{1}; // okay
X c = 1; // errorAllows narrowing conversions unless using braces:
int x = 3.14; // allowed, x == 3, usually warningUse when the initialization is conceptually an assignment-like conversion:
std::string name = "David";
auto value = compute();Avoid when you need explicit constructors.
T x(args);
T x{args};Parentheses form:
std::string s("hello");
std::vector<int> v(10, 5); // 10 elements, each 5Brace form is technically direct-list-initialization:
std::string s{"hello"};
std::vector<int> v{1, 2, 3};Can call explicit constructors.
struct X {
explicit X(int) {}
};
X x(42); // okay
X y{42}; // okayParentheses avoid some std::initializer_list surprises:
std::vector<int> a(10); // 10 zeros
std::vector<int> b{10}; // one element: 10Parentheses can trigger the most vexing parse:
std::string s(); // function declaration, not an objectBraces can prefer std::initializer_list overloads unexpectedly:
std::vector<int> v{10}; // one element, not tenUse parentheses when you want a specific constructor overload:
std::vector<int> v(100, 0);
std::lock_guard<std::mutex> lock(m);Use braces when you want uniform, narrowing-safe initialization:
Point p{1, 2};
std::string s{"hello"};There are two major forms:
T x{args}; // direct-list-initialization
T x = {args}; // copy-list-initializationExamples:
int x{42};
int y = {42};
std::vector<int> v{1, 2, 3};
struct Point {
int x;
int y;
};
Point p{10, 20};int a = 3.14; // allowed, usually warning
int b{3.14}; // errorAlso:
char c{1000}; // errorPrevents narrowing.
Works for aggregates.
Works well with containers.
std::array<int, 3> a{1, 2, 3};
std::vector<int> v{1, 2, 3};Often avoids the most vexing parse:
std::string s{}; // objectstd::initializer_list constructors are preferred.
std::vector<int> a(10, 20); // 10 elements, each 20
std::vector<int> b{10, 20}; // 2 elements: 10 and 20This can surprise people.
Another example:
std::vector<int> v1(5); // five zeros
std::vector<int> v2{5}; // one element: 5Use braces for aggregates, simple values, and narrowing protection:
int x{42};
Point p{1, 2};
std::array<int, 3> arr{1, 2, 3};Be careful with types that have std::initializer_list constructors, especially containers.
An aggregate is a simple class/struct/array-like type with no user-declared constructors and some other restrictions.
struct Point {
int x;
int y;
};
Point p{1, 2};Members are initialized in declaration order:
struct Person {
std::string name;
int age;
};
Person p{"David", 32};Missing members are value-initialized:
Point p{1}; // x = 1, y = 0Arrays are also aggregates:
int values[3]{1, 2, 3};Simple, efficient, readable.
Great for data-only types.
struct Config {
int port;
bool verbose;
std::string host;
};
Config c{8080, true, "localhost"};Initialization order follows member declaration order, not visual intent.
struct Rectangle {
int width;
int height;
};
Rectangle r{100, 50};Fine here, but fragile if members are reordered.
Before C++20, positional aggregate initialization can be unclear for larger structs.
Use for simple value types, configuration structs, DTOs, and POD-like data.
struct Config {
std::string host;
int port;
bool verbose;
};
Config c{
.host = "localhost",
.port = 8080,
.verbose = true
};Much clearer than positional aggregate initialization.
Config c{.host = "localhost", .port = 8080, .verbose = true};Reduces mistakes with same-type fields:
struct Rectangle {
int width;
int height;
};
Rectangle r{.width = 100, .height = 50};Designators must appear in declaration order.
Config c{
.port = 8080,
.host = "localhost" // error: wrong order
};Only works for aggregates.
Not as flexible as C designated initializers.
Use for larger aggregate structs where positional initialization is unclear.
Excellent for config objects.
int x = 10;
int& r = x;
const int& cr = 42;
int&& rr = 42;References must be initialized immediately.
int& ref; // errorA non-const lvalue reference cannot bind to a temporary:
int& r = 42; // errorBut a const lvalue reference can:
const int& r = 42; // okay, lifetime extendedRvalue references bind to rvalues:
std::string&& s = std::string{"hello"};Enables aliases, efficient parameter passing, and lifetime extension for const references.
const std::string& name = getName();Can create dangling references:
const std::string& bad = getTemporaryString();
// okay only if lifetime is extended directly;
// dangerous in more complex casesReferences cannot be reseated.
int a = 1;
int b = 2;
int& r = a;
r = b; // assigns b's value to a; does not rebind rUse for function parameters, aliases, and avoiding copies.
void print(const std::string& s);
void mutate(std::vector<int>& v);Inside constructors, members can be initialized using a member initializer list:
class Person {
public:
Person(std::string name, int age)
: name_{std::move(name)}, age_{age}
{
}
private:
std::string name_;
int age_;
};This is different from assignment inside the constructor body:
class Person {
public:
Person(std::string name, int age)
{
name_ = std::move(name); // assignment, not initialization
age_ = age;
}
private:
std::string name_;
int age_;
};Required for references, const members, and members without default constructors.
class X {
public:
X(int& r, int value)
: ref_{r}, value_{value}
{
}
private:
int& ref_;
const int value_;
};More efficient for class members because it constructs directly.
Members are initialized in declaration order, not initializer-list order.
class X {
int a_;
int b_;
public:
X() : b_{1}, a_{b_} {} // bad: a_ initialized before b_
};Almost always initialize data members in the constructor initializer list.
struct Options {
int port = 8080;
bool verbose = false;
std::string host = "localhost";
};Can be overridden by constructors or aggregate initialization:
Options a; // port 8080
Options b{.port = 9000}; // port 9000, others defaultKeeps defaults near the member declarations.
Excellent for config-like types.
Prevents uninitialized members.
Can interact subtly with constructors if some constructors override defaults and others do not.
Use for sensible member defaults.
class SocketOptions {
int timeoutMs_ = 5000;
bool reuseAddress_ = true;
};T b = a;Example:
std::string a = "hello";
std::string b = a; // copy constructionFor move:
std::string c = std::move(a); // move constructionReadable.
Natural for copying and moving objects.
May be expensive if copying large objects.
Some types are non-copyable:
std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::unique_ptr<int> p2 = p1; // error
std::unique_ptr<int> p3 = std::move(p1); // okayUse when you intentionally want a copy or move.
T b(a);
T b{a};Example:
std::string a{"hello"};
std::string b(a);
std::string c{a};Can be explicit and constructor-like.
Usually less idiomatic for ordinary copying than:
T b = a;Useful in generic code or when you want constructor syntax.
Move initialization is not a separate syntax category in the standard, but practically it means constructing an object from an rvalue.
std::string a = "hello";
std::string b = std::move(a);Or:
std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::unique_ptr<int> p2 = std::move(p1);Efficient transfer of resources.
Essential for move-only types like:
std::unique_ptr<T>
std::thread
std::fstream
std::mutex // non-movable, but resource-owningThe moved-from object is valid but usually unspecified in value.
std::string s = "hello";
std::string t = std::move(s);
// s is valid, but do not rely on its old contentsUse when transferring ownership or avoiding expensive copies.
T* p = new T;
T* q = new T();
T* r = new T{};
T* arr = new T[10]{};Important differences:
int* a = new int; // uninitialized
int* b = new int(); // zero-initialized
int* c = new int{}; // zero-initializedFor arrays:
int* arr1 = new int[10]; // elements uninitialized
int* arr2 = new int[10]{}; // elements zero-initializedAllows dynamic lifetime.
Manual new/delete is error-prone.
delete p;
delete[] arr;Leaks, double deletes, and exception-safety problems are easy.
Prefer smart pointers and containers:
auto p = std::make_unique<T>();
auto shared = std::make_shared<T>();
std::vector<int> values(10);Use raw new rarely in modern C++.
auto x = 42; // int
auto y = 3.14; // double
auto s = "hello"; // const char*With braces, rules can surprise:
auto a = {1, 2, 3}; // std::initializer_list<int>
auto b{1}; // int
// auto c{1, 2}; // errorAvoids repetition.
Good for complex iterator or template types.
auto it = map.find(key);
auto ptr = std::make_unique<Foo>();Can deduce unexpected types.
auto s = "hello"; // const char*, not std::string
auto x = 1u; // unsigned intUse when the type is obvious or not important:
auto result = compute();
auto socket = makeSocket();Avoid when the exact type matters for readability or correctness.
Since C++17:
std::pair p{1, 2.5}; // std::pair<int, double>
std::vector v{1, 2, 3}; // std::vector<int>
std::array a{1, 2, 3}; // std::array<int, 3>Reduces verbosity.
std::pair p{"age", 32};Can deduce surprising types.
std::vector v{10}; // vector<int> with one element 10Sometimes explicit type is clearer:
std::vector<int> v(10); // ten intsUse when deduction is obvious and improves readability.
Avoid when constructor overloads make meaning ambiguous.
For objects with static or thread storage duration, initialization can happen at compile time if the initializer is a constant expression.
constinit int x = 42;
constexpr int square(int x) {
return x * x;
}
constinit int y = square(5);constinit requires compile-time initialization:
constinit int global = 10;Avoids static initialization order problems.
No runtime initialization cost.
Good for global constants and low-level systems code.
Only works when the initializer is a constant expression.
Use constexpr for compile-time constants:
constexpr int maxConnections = 128;Use constinit for global/static variables that must be initialized before runtime dynamic initialization:
constinit std::atomic<int> counter{0};std::string globalName = computeName();This happens before main, but the order across translation units is tricky.
// file1.cpp
std::string a = getA();
// file2.cpp
std::string b = useA();The relative order may be unspecified across translation units.
Allows complex global objects.
Can cause the static initialization order fiasco.
Avoid complex global objects if possible.
Prefer function-local statics:
const std::string& name() {
static const std::string value = computeName();
return value;
}Since C++11, function-local static initialization is thread-safe.
int a[3]; // uninitialized local array
int b[3]{}; // all zero
int c[3]{1, 2}; // 1, 2, 0
int d[]{1, 2, 3}; // size deduced: 3For std::array:
std::array<int, 3> a{}; // all zero
std::array<int, 3> b{1, 2, 3};Brace initialization is clean and safe.
Raw arrays do not know their size easily and decay to pointers.
Prefer std::array for fixed-size arrays:
std::array<int, 3> values{1, 2, 3};Prefer std::vector for dynamic-size arrays:
std::vector<int> values(100);enum class Color {
Red,
Green,
Blue
};
Color c{Color::Red};For scoped enums with fixed underlying type:
enum class Flags : unsigned {
None = 0,
Read = 1,
Write = 2
};
Flags f{Flags::Read};Since C++17, direct-list-initialization can initialize scoped enums from integer values under restrictions:
enum class Byte : unsigned char {};
Byte b{42}; // okay if non-narrowingScoped enums avoid implicit integer conversions.
Integer initialization of enums can reduce type safety if overused.
Prefer named enumerators:
Color c = Color::Red;A constructor taking std::initializer_list<T> enables syntax like:
class Numbers {
public:
Numbers(std::initializer_list<int> values) {}
};
Numbers n{1, 2, 3};Standard containers use this:
std::vector<int> v{1, 2, 3};
std::set<int> s{3, 1, 2};Great for list-like types.
Matrix m{
1, 2, 3,
4, 5, 6
};std::initializer_list overloads are preferred over other constructors during list initialization.
Classic example:
std::vector<int> a(10, 5); // ten elements of value 5
std::vector<int> b{10, 5}; // two elements: 10 and 5Also, elements in std::initializer_list are const, which can affect move-only types.
std::vector<std::unique_ptr<int>> v{
std::make_unique<int>(1)
}; // problematic because initializer_list elements are constUse for list-like APIs.
Avoid adding initializer_list constructors casually because they strongly affect overload resolution.
Initialization:
std::string s{"hello"};Assignment:
std::string s;
s = "hello";These are not the same.
Initialization constructs the object directly.
Assignment first constructs an object, then changes its value.
For members:
class X {
public:
X(std::string s)
: s_{std::move(s)} // initialization
{
}
private:
std::string s_;
};Usually prefer initialization.
| Syntax | Name | Example | Notes |
|---|---|---|---|
T x; |
Default initialization | int x; |
Dangerous for local primitives |
T x{}; |
Value/list initialization | int x{}; |
Usually safest |
T x = expr; |
Copy initialization | std::string s = "hi"; |
Ignores explicit constructors |
T x(expr); |
Direct initialization | std::vector<int> v(10); |
Can hit most vexing parse |
T x{expr}; |
Direct-list initialization | int x{42}; |
Prevents narrowing |
T x = {expr}; |
Copy-list initialization | int x = {42}; |
Prevents narrowing, but stricter with explicit |
T x{a, b, c}; |
Aggregate/list initialization | Point p{1, 2}; |
Great for simple structs |
T x{.a=1} |
Designated initialization | Config c{.port=80}; |
C++20, aggregates only |
static T x; |
Static zero/default initialization | static int x; |
Zero-initialized first |
constinit T x = ...; |
Constant initialization required | constinit int x = 5; |
Compile-time initialization |
For modern C++, a good default style is:
int x{};
double d{};
std::string s{};
Point p{1, 2};
std::vector<int> values{1, 2, 3};Use parentheses when constructor meaning matters:
std::vector<int> values(100, 0); // 100 zeros
std::string s(10, 'x'); // "xxxxxxxxxx"Use constructor initializer lists for class members:
class User {
public:
User(std::string name, int age)
: name_{std::move(name)}, age_{age}
{
}
private:
std::string name_;
int age_{};
};Best rule of thumb:
T x{}; // safest default
T x{args}; // good for aggregates and simple construction
T x(args); // use when selecting a specific constructor matters
T x = expr; // use when conversion/copy style is clearerThe biggest trap to remember is this:
std::vector<int> a(10); // 10 elements
std::vector<int> b{10}; // 1 element- Introduction
- What “Initialization” Actually Means
- Initialization vs Assignment
- The Historical Evolution of Initialization in C++
- The Core Categories of Initialization
- Default Initialization
- Value Initialization
- Zero Initialization
- Copy Initialization
- Direct Initialization
- List Initialization
- Uniform Initialization
- Direct-List Initialization
- Copy-List Initialization
- Aggregate Initialization
- Designated Initialization (C++20)
- Reference Initialization
- Constant Initialization
- Static Initialization
- Dynamic Initialization
- Ordered vs Unordered Dynamic Initialization
- The Static Initialization Order Fiasco
- Constructor Member Initialization Lists
- Default Member Initializers
- Delegating Constructors
- Inheriting Constructors
- Array Initialization
- String Literal Initialization
- Enum Initialization
- Union Initialization
- Bit-field Initialization
- POD Initialization
- Trivial Initialization
- Functional-Style Initialization
- Empty Brace Initialization
- Narrowing Conversions
- std::initializer_list Semantics
- Overload Resolution with Brace Initialization
- Copy Elision and Guaranteed Copy Elision
- Temporary Materialization
- Lifetime Extension
- Move Initialization
- Initialization and Value Categories
- Perfect Forwarding and Initialization
- CTAD and Deduction-Based Initialization
- Auto Initialization Rules
- Initialization in Templates
- Initialization of constexpr Objects
- constinit and consteval
- Dynamic Allocation Initialization
- Placement New Initialization
- Initialization of Atomics
- Thread-Local Initialization
- Exception Safety During Initialization
- Initialization Order Rules
- The Most Vexing Parse
- Brace Elision
- Common Pitfalls
- Modern Best Practices
- Recommended Style Guide
- Summary Cheat Sheet
Initialization is one of the deepest and most important parts of C++.
It affects:
- correctness
- performance
- overload resolution
- type safety
- object lifetime
- move semantics
- template deduction
- constexpr evaluation
- exception safety
- ABI behavior
- low-level memory semantics
Many bugs in professional C++ code are fundamentally initialization bugs.
Examples include:
- reading uninitialized memory
- incorrect constructor overload selection
- accidental narrowing conversions
- static initialization order issues
- dangling references
- unexpected copies
- accidental
std::initializer_listselection - incorrect move behavior
- lifetime extension misunderstandings
Modern C++ initialization is also notoriously complicated because the language accumulated multiple systems over decades:
- C initialization rules
- C++98 constructor semantics
- POD rules
- C++11 uniform initialization
- move semantics
- list initialization
- guaranteed copy elision
- designated initialization
- constexpr/constinit systems
Understanding initialization thoroughly is essential for advanced modern C++.
Initialization is the process of giving an object its initial state at the moment it is created.
Example:
int x = 5;Here:
- storage is acquired for
x xbegins its lifetimexreceives its initial value
Initialization is NOT assignment.
This distinction is fundamental.
Initialization creates an object.
Assignment modifies an existing object.
Initialization:
std::string s{"hello"};Assignment:
std::string s;
s = "hello";The second version:
- constructs an empty string
- later assigns
"hello"
This can have different:
- performance
- semantics
- exception behavior
For classes, initialization invokes constructors.
Assignment invokes assignment operators.
C mostly had:
- zero initialization
- aggregate initialization
- assignment-style initialization
Example:
int x = 5;
int arr[3] = {1,2,3};C++ introduced:
- constructors
- overload resolution
- references
- class initialization
Example:
std::string s("hello");C++11 radically changed initialization with:
- brace initialization
- move semantics
- initializer lists
- delegating constructors
- default member initializers
This introduced “uniform initialization”.
Major additions:
- guaranteed copy elision
- CTAD
- inline variables
- designated initialization
- constinit
- improved constexpr
The official standard categories are roughly:
- default initialization
- value initialization
- zero initialization
- copy initialization
- direct initialization
- list initialization
- aggregate initialization
- reference initialization
- constant initialization
Modern C++ additionally relies heavily on:
- direct-list initialization
- copy-list initialization
- uniform initialization
Syntax:
T obj;Examples:
int x;
std::string s;Behavior depends on type.
Local primitives are NOT initialized.
int x;x has an indeterminate value.
Reading it is undefined behavior.
Constructors are called.
std::string s;Calls default constructor.
Each element is default-initialized.
int arr[10];Each element is uninitialized.
Efficient.
No unnecessary work.
Dangerous for primitive types.
Very common source of bugs.
Avoid default-initialized primitive locals unless intentional.
Prefer:
int x{};Value initialization typically occurs with:
T obj{};
T()Examples:
int x{};
double d{};
std::string s{};Zero-initialized.
int x{}; // 0Default constructor called.
Members recursively value-initialized.
struct Point {
int x;
int y;
};
Point p{};Result:
p.x == 0
p.y == 0This is one of the safest forms of initialization.
int x{};avoids UB from uninitialized variables.
Zero initialization sets storage to zero bits conceptually.
Examples:
static int x;
int y{};Effects:
0
0.0
false
nullptrZero initialization is usually a stage within another initialization sequence.
Syntax:
T obj = expr;Examples:
int x = 5;
std::string s = "hello";Despite the name, no copy may actually occur.
Copy initialization ignores explicit constructors.
Example:
class X {
public:
explicit X(int);
};
X a(5); // OK
X b = 5; // ERRORSyntax:
T obj(args);Example:
std::string s("hello");Unlike copy initialization, explicit constructors participate.
Direct initialization is often used to precisely select constructors.
Example:
std::vector<int> v(10, 5);Creates:
- 10 elements
- each equal to 5
Brace-based initialization:
T obj{args};
T obj = {args};Introduced in C++11.
“Uniform initialization” is the informal name for brace initialization syntax intended to unify initialization across all types.
Examples:
int x{5};
std::string s{"hello"};
std::vector<int> v{1,2,3};
Point p{1,2};Goals:
- consistency
- narrowing prevention
- avoid vexing parse
- support aggregates and classes uniformly
Syntax:
T obj{args};Example:
std::vector<int> v{1,2,3};Explicit constructors ARE considered.
Syntax:
T obj = {args};Explicit constructors are NOT considered.
Applies to aggregates.
Example:
struct Point {
int x;
int y;
};
Point p{1,2};Members initialize in declaration order.
Missing elements are value-initialized.
Point p{1};Equivalent to:
x = 1
y = 0Syntax:
Point p{
.x = 10,
.y = 20
};Improves readability.
Prevents ordering mistakes.
Excellent for config structures.
Only for aggregates.
Order must match declaration order.
References must always be initialized.
int x = 5;
int& r = x;Can bind temporaries.
const int& r = 5;int&& rr = 5;Occurs when an object can be initialized at compile time.
constexpr int x = 5;
constinit int y = 10;Very important for globals/statics.
Static storage duration objects undergo:
- zero initialization
- constant initialization if possible
before dynamic initialization.
Initialization requiring runtime code.
Example:
std::string s = compute();Occurs before main() for globals.
Within a translation unit:
- ordered
Across translation units:
- partially unordered
This creates major problems.
Classic C++ issue.
Example:
// file1.cpp
Logger logger;
// file2.cpp
Config config(logger);Initialization order across files is unspecified.
Can cause crashes.
Use function-local statics:
Logger& GetLogger() {
static Logger logger;
return logger;
}Thread-safe since C++11.
Syntax:
class X {
public:
X(int v)
: value(v)
{
}
private:
int value;
};Members initialize in declaration order, NOT initializer-list order.
Syntax:
class X {
int value = 42;
};Modern and highly recommended.
One constructor calls another.
class X {
public:
X() : X(42) {}
X(int value) {}
};class Base {
public:
Base(int);
};
class Derived : public Base {
public:
using Base::Base;
};int arr[3]{1,2,3};Partial initialization:
int arr[5]{1,2};Remaining elements become zero.
char str[] = "hello";Includes null terminator.
Equivalent size:
char str[6];Scoped enums:
enum class Color {
Red
};
Color c{Color::Red};Only one member active at a time.
union U {
int x;
float y;
};
U u{42};Activates x.
struct Flags {
unsigned a : 1;
unsigned b : 2;
};
Flags f{1,2};Historical term.
POD = Plain Old Data.
Modern C++ split POD into:
- trivial
- standard-layout
Important for low-level systems programming.
Trivial types can often be safely memcpy’d.
Syntax:
int x(5);Looks like function-call syntax.
Very important modern pattern:
T obj{};Examples:
int x{};
double d{};
int* p{};Safe and predictable.
Brace initialization forbids narrowing.
Example:
int x{3.14}; // ERRORThis is one of the biggest advantages of braces.
Special constructor support:
std::vector<int> v{1,2,3};Internally uses:
std::initializer_list<int>Critical topic.
Example:
std::vector<int> a(10,5);
std::vector<int> b{10,5};Completely different meaning.
Brace initialization strongly prefers initializer_list constructors.
Pre-C++17:
T x = T();Temporary may exist.
Since C++17:
Often no temporary exists at all.
Example:
T Make() {
return T{};
}Returned object constructed directly.
No copy.
No move.
Modern standard concept.
Prvalues no longer necessarily create temporary objects immediately.
Temporary objects materialize only when needed.
Advanced but important.
Example:
const std::string& r = std::string("hello");Temporary lifetime extended to match reference lifetime.
Initialization from rvalues.
std::string a = "hello";
std::string b = std::move(a);Transfers resources efficiently.
Initialization behavior depends heavily on:
- lvalues
- xvalues
- prvalues
Move semantics fundamentally rely on value categories.
Templates often preserve initialization categories:
template<typename T>
void f(T&& value);Uses forwarding references.
Class Template Argument Deduction:
std::pair p(1,2.0);Compiler deduces:
std::pair<int,double>auto x = 5;Deduction depends on initializer.
Brace rules are special:
auto x{5}; // int
auto y = {5}; // initializer_list<int>Templates make initialization extremely complicated because deduction interacts with constructors and overload resolution.
constexpr int x = 5;Must be initialized with constant expressions.
Requires compile-time initialization.
constinit int x = 5;Immediate compile-time evaluation.
new int;
new int();
new int{};Different semantics.
new int;Uninitialized.
new int{};Zero-initialized.
Constructs objects in preallocated memory.
new(ptr) T(args);Critical for allocators and low-level systems code.
std::atomic<int> x{0};Very important in concurrent programming.
thread_local int x = 5;Each thread gets separate instance.
If constructor throws:
- partially constructed members are destroyed
- fully constructed bases are destroyed
Important RAII guarantee.
Critical rule:
Members initialize in declaration order.
NOT constructor list order.
Classic syntax ambiguity.
std::string s();Declares function.
Not object.
Brace initialization helps avoid this.
Nested braces may be omitted.
int arr[2][2] = {
1,2,
3,4
};int x;std::vector<int> v{10};One element.
NOT ten elements.
Global object dependencies are dangerous.
const std::string& r = GetString();Can be dangerous.
int x{3.14}; // errorint x{};
std::string s{};class X {
int value = 0;
};Especially primitives.
Avoid global initialization order issues.
Especially containers.
T obj{};std::vector<int> v(10,5);Bad:
int x;Good:
int x{};Avoid manual initialization management.
| Syntax | Category | Notes |
|---|---|---|
T x; |
Default initialization | Dangerous for primitives |
T x{}; |
Value/list initialization | Safest general form |
T x = expr; |
Copy initialization | Ignores explicit ctors |
T x(expr); |
Direct initialization | Selects constructors directly |
T x{expr}; |
Direct-list initialization | Prevents narrowing |
T x = {expr}; |
Copy-list initialization | Stricter overload rules |
Point p{1,2}; |
Aggregate initialization | Simple structs |
Point p{.x=1}; |
Designated initialization | C++20 |
new T; |
Default initialization | Possibly uninitialized |
new T{}; |
Value initialization | Zero/value initialized |
constinit int x=5; |
Constant initialization | Guaranteed static initialization |
thread_local int x; |
Thread-local initialization | Per-thread instance |
Initialization is not merely syntax in C++.
It is deeply tied to:
- type systems
- overload resolution
- memory models
- object lifetime
- move semantics
- template deduction
- ABI behavior
- optimization
- compile-time evaluation
Modern C++ strongly favors:
T obj{};because it is:
- safe
- consistent
- narrowing-resistant
- readable
- compatible with modern idioms
But expert C++ developers must still understand every initialization category because overload resolution and lifetime rules can radically change program behavior based on small syntax differences.
Initialization is one of the core pillars of mastering modern C++.
For this exact case:
int i = 5;
int i{5};
int i(5);all three create the same object:
i == 5For a plain int initialized with the literal 5, there is no performance difference, no runtime difference, and no generated-code difference in any sane compiler.
The difference is about:
- safety,
- consistency,
- readability,
- generic applicability,
- narrowing behavior,
- overload behavior for class types,
- and style.
My recommendation:
int i{5};is usually the best modern C++ choice.
But the full answer is more nuanced.
This is copy initialization.
int i = 5;Despite the name, for int there is no “copy” happening. The value 5 initializes i directly.
It is extremely readable.
int i = 5;This looks like normal mathematical assignment, even though technically it is initialization.
It is familiar to C programmers, C++ programmers, Java programmers, C# programmers, JavaScript programmers, and almost everyone else.
For simple scalar variables, it is visually clean.
int count = 0;
double ratio = 0.5;
bool enabled = true;It also works very naturally with auto:
auto i = 5; // int
auto d = 5.0; // double
auto s = "text"; // const char*This style is especially common when the initializer is a named expression:
int result = computeResult();
std::string name = getName();
auto socket = createSocket();It reads nicely as:
“result is initialized from computeResult.”
The biggest weakness is that it allows narrowing conversions.
int i = 5.7; // allowed, i becomes 5Usually the compiler warns, but it is not ill-formed by default.
By contrast:
int i{5.7}; // errorSo this form is less safe.
Another issue is that for class types, copy initialization does not consider explicit constructors.
struct X {
explicit X(int) {}
};
X a(5); // OK
X b{5}; // OK
X c = 5; // ERRORThat can be either good or bad.
It is good if you want to avoid implicit conversions.
It is bad if you intentionally want to call an explicit constructor.
Also, the syntax visually resembles assignment:
int i = 5;But this is not assignment. It is initialization.
That distinction matters for class types:
std::string s = "hello"; // initialization, not assignmentThis can confuse beginners, though most experienced C++ programmers are used to it.
Use this form when the assignment-like style improves readability:
int port = 8080;
bool connected = false;
auto result = compute();
std::string name = "David";It is also very idiomatic with auto:
auto value = getValue();For simple constants, many codebases still prefer it:
constexpr int MaxRetries = 3;This is direct-list-initialization, also commonly associated with uniform initialization.
int i{5};This is the safest of the three for int.
The major advantage is that it prevents narrowing conversions.
int a{5}; // OK
int b{5.0}; // error
int c{5.7}; // errorThis is a real advantage. Many C++ bugs come from silent narrowing:
double price = 19.99;
int cents = price; // silently becomes 19With braces:
int cents{price}; // errorThat is usually what you want.
Brace initialization also works consistently across many kinds of types:
int i{5};
double d{3.14};
bool b{true};
std::string s{"hello"};
std::vector<int> v{1, 2, 3};It avoids the most vexing parse problem.
std::string s(); // function declaration
std::string s{}; // objectFor scalar values, brace initialization communicates:
“I am deliberately initializing this object with this value, and I want narrowing protection.”
It is especially good for modern C++ style guides that prefer uniform initialization where reasonable.
The biggest criticism is aesthetics.
Some people find this:
int i{5};less readable than:
int i = 5;For simple arithmetic values, braces can feel visually heavier or more “C++-specific.”
Another criticism is that brace initialization can behave surprisingly with class types that have std::initializer_list constructors.
Classic example:
std::vector<int> a(5); // five ints, all zero
std::vector<int> b{5}; // one int, value 5And:
std::vector<int> a(10, 20); // ten elements, each 20
std::vector<int> b{10, 20}; // two elements: 10 and 20So while braces are excellent for scalar values like int, they are not always the best universal choice for every type.
Another issue is interaction with auto:
auto a = 5; // int
auto b{5}; // int
auto c = {5}; // std::initializer_list<int>This surprises many people.
Also:
auto x = {1, 2, 3}; // std::initializer_list<int>So while int i{5}; is clear, auto x{...} and auto x = {...} require care.
For local scalar variables, this is usually excellent:
int count{0};
double ratio{0.5};
bool enabled{true};
char c{'a'};It is also excellent when avoiding narrowing is important:
std::size_t size{values.size()};Though even here, be careful: if values.size() cannot fit into the target type, braces force you to confront the issue.
For aggregates, braces are also natural:
struct Point {
int x;
int y;
};
Point p{10, 20};This is direct initialization using parentheses.
int i(5);For int, it works perfectly.
It is historically common C++ syntax.
It mirrors constructor-call syntax:
std::string s("hello");
std::vector<int> v(10, 0);
std::lock_guard<std::mutex> lock(m);For class types, parentheses are often the best choice when you specifically want to call a particular constructor overload.
Example:
std::vector<int> v(10, 5);This means:
create a vector with 10 elements, each equal to 5.
With braces:
std::vector<int> v{10, 5};This means:
create a vector with two elements: 10 and 5.
So parentheses are essential in many contexts.
For int i(5);, there is no ambiguity and no performance cost.
For scalar variables, it is usually less idiomatic in modern C++.
int i(5);looks a little like a function-style cast or constructor call. Since int is not a class, some people find it unnecessarily “ceremonial.”
It also does not prevent narrowing:
int i(5.7); // allowed, i becomes 5So it is less safe than:
int i{5.7}; // errorAnother major issue is the most vexing parse, though not for this exact example.
This:
std::string s();does not create a string.
It declares a function named s returning std::string.
Parentheses are vulnerable to this syntactic ambiguity in some cases.
Braces avoid that:
std::string s{};For int i(5);, the most vexing parse is not a problem. But as a general initialization style, parentheses have this historical hazard.
Use parentheses when constructor overload selection matters.
Excellent:
std::vector<int> values(100, 0);
std::string line(80, '-');
std::unique_lock<std::mutex> lock(mutex, std::defer_lock);Less compelling:
int i(5);
double d(3.14);
bool b(true);For primitive values, braces or = are usually clearer.
All equivalent:
int a = 5;
int b{5};
int c(5);No meaningful runtime difference.
The compiler will produce the same machine code.
Different:
int a = 5.7; // allowed
int b{5.7}; // error
int c(5.7); // allowedThis is the strongest argument for braces.
Many people find this most natural:
int i = 5;Many modern C++ programmers prefer:
int i{5};Fewer people today prefer:
int i(5);for primitive types.
Braces are most consistent:
int i{5};
double d{3.14};
std::string s{"hello"};
Point p{1, 2};But parentheses are more precise for certain constructor calls:
std::vector<int> v(10, 5);And = is often most readable with auto:
auto result = compute();For this exact case, I would rank them:
int i{5};Reason:
- prevents narrowing,
- clearly initializes,
- works well with modern C++ style,
- no performance downside,
- avoids assignment-looking syntax.
int i = 5;Reason:
- extremely readable,
- familiar,
- idiomatic,
- especially good for simple scalar values.
Weakness:
- allows narrowing.
int i(5);Reason:
- works perfectly,
- historically valid,
- useful for constructors.
Weakness:
- allows narrowing,
- less idiomatic for simple scalars,
- parenthesized initialization has broader syntactic pitfalls.
For ordinary modern C++ code, I would use:
int i{5};For zero initialization:
int i{};For computed initialization:
int count{computeCount()};But if a codebase uses assignment-style initialization for scalars, this is also perfectly reasonable:
int i = 5;I would avoid this for plain scalars unless following an existing style:
int i(5);With auto, I usually prefer =:
auto i = 5;
auto name = getName();
auto socket = makeSocket();Rather than:
auto i{5};Although auto i{5}; is valid and deduces int, auto plus braces has enough historical weirdness that auto x = expr; is usually clearer.
Especially avoid:
auto x = {5};unless you really want:
std::initializer_list<int>For class types, do not blindly prefer braces.
Example:
std::vector<int> a(5);
std::vector<int> b{5};These are different.
a has five elements.
b has one element.
So this rule is too simplistic:
Always use braces.
A better rule is:
Use braces for scalar values, aggregates, and narrowing protection. Use parentheses when constructor overload selection matters.
For:
int i = 5;
int i{5};
int i(5);the best modern choice is usually:
int i{5};because it is the safest and most explicit initialization form.
But:
int i = 5;is also highly idiomatic, readable, and completely acceptable.
I would reserve:
int i(5);mostly for cases where the type is class-like and constructor-call syntax is useful.
The most practical rule is:
int i{5}; // best modern scalar initialization
auto x = expression; // best common auto initialization
T obj(args); // use when selecting a constructor matters
T obj{args}; // use for aggregates, scalars, and narrowing safetyInitialization Philosophy in C, C++, and Rust: Why They Look Different and What Each Language Optimizes For
When people compare:
// C++
int x = 5;
int y{5};
int z(5);to:
/* C */
int x = 5;or:
// Rust
let x = 5;they often focus on syntax.
The syntax is actually the least interesting part.
The real story is that C, C++, and Rust have fundamentally different philosophies about object creation, type conversion, safety, and abstraction.
Their initialization systems are direct consequences of those philosophies.
C:
Initialization should be simple, predictable, and close to the machine.
C++:
Initialization should support both simple machine-level objects and arbitrarily sophisticated user-defined types without sacrificing performance.
Rust:
Initialization should guarantee safety and correctness even if that requires restricting programmer freedom.
Everything else flows from those goals.
In C, initialization is primarily about assigning bits to storage.
Example:
int x = 5;
double y = 3.14;There are no constructors.
There are no user-defined initialization rules.
There is no overload resolution.
There are no move constructors.
There are no explicit constructors.
There is no RAII.
There are no temporary lifetime rules.
There is no distinction between:
int x = 5;and:
int x;
x = 5;except timing.
The type system is extremely simple.
C was designed for:
- operating systems
- compilers
- embedded systems
- hardware access
The philosophy was:
"Trust the programmer."
and
"Don't pay for what you don't use."
C initialization therefore tries to be:
- small
- obvious
- unsurprising
- easy to compile
Example:
struct Point {
int x;
int y;
};
struct Point p = {1, 2};There is no hidden code.
No constructor is called.
The compiler simply lays out memory and stores values.
C's simplicity comes with costs.
Consider:
int x;
printf("%d\n", x);Undefined behavior.
The language trusts you.
Or:
double d = 3.14;
int i = d;Allowed.
Possible loss of information.
Again:
programmer's responsibility
C++ inherited C.
Originally:
int x = 5;worked almost exactly like C.
Then C++ added classes:
class String {
public:
String(const char*);
};Now:
String s = "hello";must somehow invoke code.
Suddenly initialization became much more than putting bits into memory.
How do you make this:
int x = 5;and this:
std::string s = "hello";feel similar?
Even though one is a primitive value and the other runs arbitrary code.
That challenge drives much of C++ initialization complexity.
One of the most important ideas in C++:
User-defined types should behave like built-in types.
Bjarne Stroustrup often describes this as supporting abstraction without overhead.
For example:
int x = 5;
Complex c = 5;Both should feel natural.
That means initialization must support:
- primitive types
- classes
- templates
- containers
- smart pointers
- user-defined abstractions
using similar syntax.
C++ is trying to satisfy conflicting goals.
It wants:
int x = 5;std::string s("hello");T obj(args);int x{5};Point p{1, 2};std::vector<int> v{1, 2, 3};Each requirement pushed the language toward additional initialization forms.
C++11 introduced braces:
int x{5};
std::string s{"hello"};
Point p{1, 2};The goal was:
One syntax for everything.
This was a reaction to decades of complexity.
Before C++11:
int x = 5;
std::string s("hello");
Point p = {1, 2};Different categories required different syntax.
Braces attempted to unify them.
The famous example:
std::vector<int> a(10);
std::vector<int> b{10};These mean different things.
This happens because C++ must preserve backward compatibility.
If C++ had been designed from scratch, braces could probably have become truly universal.
But compatibility constraints prevented that.
If there is one word describing C++ initialization:
Flexibility
The language tries very hard to allow:
Widget w;
Widget w{};
Widget w();
Widget w(5);
Widget w{5};
Widget w = 5;
Widget w = {5};Different forms may intentionally mean different things.
The language gives the programmer enormous control.
Rust starts from a different premise.
Instead of:
Give programmers every possible tool.
Rust asks:
Which tools prevent bugs?
This dramatically affects initialization.
Rust's philosophy is:
Invalid states should be difficult or impossible to represent.
Initialization is part of that.
In C:
int x;Allowed.
In C++:
int x;Allowed.
In Rust:
let x;
println!("{}", x);Compile error.
Rust refuses to compile.
The compiler guarantees:
Every variable is initialized before use.
This is one of Rust's strongest safety properties.
In C++:
std::string s("hello");exists because classes need constructors.
Rust doesn't have constructors in the C++ sense.
Instead:
let s = String::from("hello");or:
let s = String::new();Initialization happens through functions and associated methods.
C++:
Widget();
Widget(int);
Widget(int, int);
Widget(std::string);Rust intentionally avoids this style.
Instead:
Widget::new()
Widget::with_capacity()
Widget::from_string()The motivation:
Explicitness.
Reading code should reveal intent immediately.
Consider:
std::vector<int> v(10);A newcomer may not know what this means.
Ten elements?
Capacity ten?
Value ten?
Rust prefers:
let v = Vec::with_capacity(10);The meaning is explicit.
Rust often sacrifices brevity to improve clarity.
Example:
struct User {
name: String,
age: u32,
}Creation:
let user = User {
name: "David".into(),
age: 32,
};All fields must be initialized.
You cannot accidentally forget one.
This is fundamentally different from C.
Primary goal:
Control and simplicity
Trusts programmer.
Allows dangerous operations.
Minimal language complexity.
Primary goal:
Zero-cost abstraction
Supports both:
intand
std::vector<std::unique_ptr<MyType>>using a unified language.
Accepts complexity to achieve flexibility.
Primary goal:
Safety through the type system
Restricts programmer freedom.
Uses compiler checks aggressively.
Prefers explicit APIs.
Because C++ values:
Widget w(5);
Widget w{5};
Widget w = 5;as distinct expressive tools.
Rust deliberately rejects that design.
Rust would rather force:
Widget::new(5)or
Widget::from(5)because there is only one interpretation.
C never tried to solve the abstraction problem.
There are no constructors.
No user-defined initialization semantics.
No overload resolution.
No move semantics.
No templates.
The initialization system therefore remains small.
Advantages:
- Simple
- Predictable
- Tiny language surface
Disadvantages:
- Easy to make mistakes
- Uninitialized memory common
- Weak type safety
Advantages:
- Extremely expressive
- Supports sophisticated abstractions
- Zero-cost abstractions
- Powerful generic programming
Disadvantages:
- Complex rules
- Multiple initialization forms
- Overload resolution surprises
- Steep learning curve
Advantages:
- Strong safety guarantees
- No uninitialized variable bugs
- Clear initialization APIs
- Easier reasoning about ownership
Disadvantages:
- Less flexible
- More verbose
- Some patterns require more boilerplate
- Less control over low-level shortcuts
The deepest philosophical difference is this:
The programmer is responsible for correctness.
The programmer is responsible for correctness, but the language should provide powerful abstractions.
The compiler should enforce correctness whenever possible.
That single design choice explains nearly every difference in initialization syntax, initialization safety, constructor design, and object creation across the three languages.
Uniform Initialization vs Direct-List Initialization in C++: One Is a Philosophy, the Other Is a Language Rule
This is one of the most commonly misunderstood pieces of C++ terminology.
Many developers use the terms interchangeably, but they are not the same thing.
The short answer is:
Uniform initialization is an informal design concept.
Direct-list-initialization is a specific initialization category defined by the C++ standard.
Consider:
int a{42};
std::string s{"hello"};
Point p{1, 2};These examples are:
- examples of uniform initialization (informally)
- examples of direct-list-initialization (formally)
The first term describes the idea.
The second term describes the actual language mechanism.
When C++11 was being designed, one goal was:
"Why do different types require different initialization syntax?"
Before C++11:
int x = 5;
std::string s("hello");
Point p = {1, 2};Different kinds of objects required different syntax.
The language designers wanted a more unified approach:
int x{5};
std::string s{"hello"};
Point p{1, 2};This idea became known as:
Uniform Initialization
The goal was:
- one syntax
- for all types
- everywhere
Hence:
T obj{args};would become the preferred general-purpose initialization syntax.
Because the same syntax works for:
int x{5};
double d{3.14};std::string s{"hello"};std::vector<int> v{1, 2, 3};struct Point {
int x;
int y;
};
Point p{1, 2};int arr[]{1, 2, 3};The syntax is "uniform."
This surprises many people.
The C++ standard does not contain a formal initialization category named:
Uniform Initialization
Instead, the standard talks about:
- list-initialization
- direct-list-initialization
- copy-list-initialization
Those are the real rules.
Uniform initialization is simply a common name for the brace-based style introduced in C++11.
Direct-list-initialization is a formal language rule.
Syntax:
T obj{args};Examples:
int x{5};
std::string s{"hello"};
Point p{1, 2};The key feature is:
T obj{...};without =.
This is direct-list-initialization.
This is a different rule:
T obj = {args};Example:
int x = {5};
std::string s = {"hello"};Notice the =.
This is:
copy-list-initialization
not direct-list-initialization.
When people say:
"Use uniform initialization"
they usually mean:
T obj{args};but technically the umbrella idea includes both forms:
T obj{args}; // direct-list-init
T obj = {args}; // copy-list-initBoth are brace-based initialization styles.
Because it affects overload resolution.
Consider:
class X {
public:
explicit X(int) {}
};Direct-list-init:
X a{5};works.
Copy-list-init:
X b = {5};fails.
Why?
Because copy-list-initialization ignores explicit constructors.
This distinction is one of the reasons the standard had to define separate categories.
class Widget {
public:
explicit Widget(int) {}
};Direct-list:
Widget w{42};OK.
Copy-list:
Widget w = {42};Error.
The constructor is explicit.
When someone says:
"This uses uniform initialization."
that statement is incomplete.
You still need to know whether it is:
T obj{args};or
T obj = {args};because the compiler treats them differently.
std::vector<int> v{1, 2, 3};This is:
- uniform initialization (informally)
- direct-list-initialization (formally)
std::vector<int> v = {1, 2, 3};This is:
- uniform initialization (informally)
- copy-list-initialization (formally)
One of the great ironies of C++.
The goal was:
One syntax for everything.
But reality turned out differently.
Example:
std::vector<int> a(10);
std::vector<int> b{10};Different meanings.
a:
std::vector<int> a(10);creates:
[0,0,0,0,0,0,0,0,0,0]b:
std::vector<int> b{10};creates:
[10]So brace initialization did not completely eliminate special cases.
This is why many experienced C++ developers say:
Uniform initialization was a design goal, not a fully achieved reality.
Think of it like this:
Uniform Initialization
│
├── Direct-List-Initialization
│ T obj{args};
│
└── Copy-List-Initialization
T obj = {args};
Uniform initialization is the broad concept.
Direct-list-initialization is one specific formal rule underneath it.
If you are writing casually:
int x{5};calling it "uniform initialization" is perfectly normal.
Most C++ developers will understand.
If you are discussing language rules, overload resolution, templates, or standard wording:
Use the precise term:
int x{5};is
direct-list-initialization
and
int x = {5};is
copy-list-initialization.
For modern C++ developers:
T obj{args};is usually the form people mean when they say:
"Use uniform initialization."
But technically:
- Uniform initialization = informal design philosophy using braces.
- List initialization = official standard category.
- Direct-list-initialization =
T obj{args}; - Copy-list-initialization =
T obj = {args};
So the most accurate statement is:
Direct-list-initialization is a specific kind of list initialization, and list initialization is the formal mechanism that people commonly refer to as "uniform initialization."
One of the more surprising corners of C++ type deduction involves the interaction between auto and brace initialization. At first glance, the following declarations may appear equivalent:
auto a = {1, 2, 3};
auto b{1, 2, 3};However, they follow different deduction rules, and their behavior has even changed across C++ language versions. Understanding these differences explains why copy-list-initialization is generally preferred when using auto with brace-enclosed lists.
C++ provides two forms of list initialization:
auto x = {1, 2, 3};auto x{1, 2, 3};Although the only visible difference is the presence of =, the language treats them differently when auto is involved.
When auto is deduced from a braced-init-list using copy-list-initialization, the language has a special rule that attempts to deduce a std::initializer_list.
For example:
auto values = {1, 2, 3};is deduced as:
std::initializer_list<int> values = {1, 2, 3};This behavior is explicit, predictable, and remains unchanged across modern C++ standards.
Since C++17, direct-list-initialization follows different rules.
A single element is deduced as its ordinary type:
auto x{42}; // intMultiple elements are not allowed:
auto x{1, 2, 3}; // errorThe compiler cannot deduce a type from multiple elements in this form.
As a result, direct-list-initialization with auto is no longer a way to create an initializer_list.
Before C++17, many programmers were surprised by code such as:
int i{42}; // int
auto j{42}; // ?Most people expected j to also be an int.
Instead, in C++11 and C++14, j became:
std::initializer_list<int>This meant that simply replacing an explicit type with auto could silently change the meaning of the program.
For example:
int value{42};and
auto value{42};produced different types before C++17.
The inconsistency was considered confusing and error-prone.
In C++11 and C++14, both forms of initialization deduced std::initializer_list:
auto a = {1, 2, 3}; // std::initializer_list<int>
auto b{1, 2, 3}; // std::initializer_list<int>Likewise:
auto x = {42}; // std::initializer_list<int>
auto y{42}; // std::initializer_list<int>The direct-list form did not behave like ordinary type deduction. Instead, it always preferred std::initializer_list deduction whenever possible.
C++17 changed the rules to distinguish the two forms clearly.
auto x = {42}; // std::initializer_list<int>
auto y = {1, 2, 3}; // std::initializer_list<int>auto x{42}; // int
auto y{1, 2, 3}; // errorThe new rule makes direct-list-initialization behave much more like ordinary type deduction.
Because of the C++17 change, the exact same code can have different meanings depending on the language version.
Consider:
auto i{42};std::initializer_list<int>intThis is one of the relatively rare examples where the same source code produces a different deduced type across C++ standards.
For example:
static_assert(std::is_same_v<decltype(i), int>);- Fails in C++11/C++14
- Succeeds in C++17 and later
Meanwhile:
static_assert(
std::is_same_v<
decltype(i),
std::initializer_list<int>
>
);- Succeeds in C++11/C++14
- Fails in C++17 and later
When the intention is to create an initializer_list, the copy-list form is unambiguous:
auto values = {1, 2, 3};Its meaning is clear and consistent.
By contrast:
auto values{1, 2, 3};either:
- deduced an
initializer_listin older standards, - is ill-formed in modern standards,
- or may cause confusion for readers who are familiar with the C++17 rule changes.
For this reason, modern C++ code typically follows a simple convention:
- Use
auto x = { ... };when you want astd::initializer_list. - Use
auto x{expr};when you want ordinary type deduction from a single expression.
| Code | C++11/14 | C++17+ |
|---|---|---|
auto x{42}; |
std::initializer_list<int> |
int |
auto x{1,2,3}; |
std::initializer_list<int> |
Ill-formed |
auto x = {42}; |
std::initializer_list<int> |
std::initializer_list<int> |
auto x = {1,2,3}; |
std::initializer_list<int> |
std::initializer_list<int> |
The modern design separates two concepts:
auto x = {1, 2, 3}; // initializer_list deductionand
auto x{42}; // ordinary type deductionThis distinction makes auto more predictable, reduces surprises when refactoring, and aligns direct-list-initialization more closely with the behavior of explicitly typed variables.
My recommendation:
T object{value};as the default, but not as a religion.
Use this style guide:
int count{0};
double ratio{0.5};
bool enabled{true};
std::string name{"David"};
Point p{10, 20};Prefer braces when they make code safer, clearer, and unambiguous.
Prefer:
int retries{3};
double timeout{1.5};
bool enabled{true};
char letter{'A'};Over:
int retries = 3;
int retries(3);Why?
Because braces prevent narrowing:
int x{3.14}; // error
int y = 3.14; // allowed, bad
int z(3.14); // allowed, badFor professional code, this is a strong advantage.
Prefer:
int count{};
double total{};
bool done{};
SocketHandle handle{};
std::string name{};
std::vector<int> values{};This avoids uninitialized primitive variables.
Avoid:
int count;
double total;unless you intentionally want no initialization and can prove assignment happens before use.
Prefer:
struct Point {
int x;
int y;
};
Point p{10, 20};For larger aggregates in C++20+, prefer designated initialization:
Config cfg{
.host = "localhost",
.port = 8080,
.verbose = true
};This is excellent for configuration objects.
Prefer:
class User {
public:
User(std::string name, int age)
: name_{std::move(name)}, age_{age}
{
}
private:
std::string name_;
int age_{};
};Not:
class User {
public:
User(std::string name, int age)
{
name_ = std::move(name);
age_ = age;
}
private:
std::string name_;
int age_;
};Members should be initialized, not assigned after default construction.
Prefer:
class SocketOptions {
private:
int timeoutMs_{5000};
bool reuseAddress_{true};
bool nonBlocking_{false};
};This prevents forgotten initialization and keeps defaults near the data members.
Prefer:
auto value = compute();
auto ptr = std::make_unique<Foo>();
auto it = map.find(key);Avoid casual brace initialization with auto:
auto x = {5}; // std::initializer_list<int>This can surprise people.
This is the major exception to “prefer braces.”
Prefer:
std::vector<int> values(100, 0);
std::string line(80, '-');
std::unique_lock<std::mutex> lock(mutex, std::defer_lock);Not:
std::vector<int> values{100, 0}; // two elements: 100 and 0Braces strongly prefer std::initializer_list constructors.
So for containers, be intentional.
This is perfectly fine:
constexpr int MaxRetries = 3;
const auto result = compute();
std::string_view name = "David";Do not make code uglier just to use braces everywhere.
Professional style should optimize for correctness first, then readability.
Use:
T obj{};
T obj{value};by default.
Use:
auto obj = expression;for type deduction.
Use:
T obj(args);when you need a specific constructor overload.
Use:
T obj = expression;when assignment-like readability is clearly better and narrowing is not a concern.
For professional C++11+ development, I recommend this balanced style:
int count{};
int maxRetries{3};
double timeoutSeconds{1.5};
auto result = computeResult();
std::vector<int> values(100, 0);
std::vector<int> literals{1, 2, 3};
User user{name, age};
Config cfg{
.host = "localhost",
.port = 8080,
.verbose = true
};In one sentence:
Prefer brace initialization for safety, prefer
=withauto, prefer parentheses when selecting constructors, and never follow “always use one form” blindly.
One of the major selling points of C++11 was the introduction of brace initialization:
int i{5};
std::string s{"hello"};
Point p{1, 2};
std::vector<int> v{1, 2, 3};The idea was simple and attractive:
Use one syntax everywhere.
Before C++11, different categories of types required different initialization forms:
int i = 5;
std::string s("hello");
Point p = {1, 2};Brace initialization promised a more uniform world.
A truly uniform initialization syntax would have several benefits:
Instead of teaching:
=
()
{}you could simply teach:
{}Every type would look the same:
int x{5};
std::string s{"hello"};
Widget w{arg};Brace initialization rejects narrowing:
int x{3.14}; // errorwhich is usually desirable.
std::string s();is a function declaration.
std::string s{};is clearly an object.
The problem is that C++ was not designed from scratch in 2011.
It already had:
- decades of existing code,
- constructor overloads,
- aggregate initialization,
- conversion rules,
- container APIs,
- backward compatibility requirements.
The language could not simply redefine everything around braces.
As a result, brace initialization became another system layered on top of the existing ones.
Consider:
std::vector<int> a(10);
std::vector<int> b{10};Many developers expect these to mean the same thing.
They do not.
The first means:
[0,0,0,0,0,0,0,0,0,0]Ten elements.
The second means:
[10]One element.
This single example demonstrates why "always use braces" is not a viable universal rule.
Suppose a class has:
class Widget {
public:
Widget(int size);
Widget(std::initializer_list<int>);
};Now:
Widget a(10);
Widget b{10};may call completely different constructors.
Braces are not merely a different syntax.
They can fundamentally change program behavior.
Another example:
auto x = 5;deduces:
intBut:
auto x = {5};deduces:
std::initializer_list<int>A supposedly "uniform" syntax suddenly produces a different type.
The great irony is that the more experienced a C++ developer becomes, the less likely they are to follow a blanket rule such as:
Always use braces.
Experienced developers eventually learn that:
{}is not a universal replacement for:
()or
=Each form has distinct semantics.
Instead of replacing all initialization styles, brace initialization became another important tool.
Modern C++ developers typically end up using all of these:
int x{5}; // safety
auto y = compute(); // type deduction
std::vector<int> v(100, 0); // constructor selection
std::string name = "David"; // readabilityThis is not a failure of the language so much as a consequence of C++'s primary design goal:
Preserve compatibility while adding new capabilities.
A language designed from scratch, such as Rust, could impose a single object-construction philosophy.
C++ cannot. It must coexist with code written in 1985, 1998, 2003, 2011, 2017, 2020, and beyond.
Uniform initialization succeeded as:
- a safer initialization mechanism,
- a useful default style,
- a solution to narrowing conversions,
- a solution to the most vexing parse.
It failed as:
- a universal replacement for all other initialization forms.
The practical conclusion for professional C++ is therefore:
Prefer braces when they improve safety and clarity, but understand the semantics of all initialization forms and choose the one that best expresses your intent.
That is why experienced C++ programmers usually develop a style guide around initialization rather than adopting a single initialization syntax everywhere.
