Skip to content

Instantly share code, notes, and snippets.

@MangaD
Last active May 31, 2026 18:35
Show Gist options
  • Select an option

  • Save MangaD/c65a8ea9792c87dab900bbc46ffa3c30 to your computer and use it in GitHub Desktop.

Select an option

Save MangaD/c65a8ea9792c87dab900bbc46ffa3c30 to your computer and use it in GitHub Desktop.
C++ Initialization: A Complete Practical Guide

C++ Initialization: A Complete Practical Guide

CC0

Disclaimer: ChatGPT generated document.

Below is a practical map of C++ initialization types, with syntax, behavior, pros, cons, and typical use cases.

1. Default initialization

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 vector

For fundamental local variables, no initialization happens:

int x;      // indeterminate value
double d;   // indeterminate value

For objects with static storage duration, zero-initialization happens first:

static int x; // 0

Pros

Good for class types with meaningful default constructors.

std::mutex m;
std::string name;
std::vector<int> values;

Cons

Dangerous for primitive local variables.

int x;
std::cout << x; // undefined behavior

Use when

Use for types whose default constructor gives a valid state.

Avoid for raw primitive locals unless you will immediately assign them before reading.


2. Value initialization

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{};     // false

For class types, it calls a constructor, usually the default constructor:

std::string s{};      // empty
std::vector<int> v{}; // empty

For aggregates, it initializes members recursively:

struct Point {
    int x;
    int y;
};

Point p{}; // x = 0, y = 0

Pros

Usually the safest general-purpose initialization form.

int count{};
std::string name{};
Point p{};

Prevents uninitialized primitive values.

Cons

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: 10

Use when

Use as your default choice for local variables:

int retries{};
double total{};
std::string label{};

3. Zero initialization

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{};         // nullptr

For class objects, zero-initialization may happen before default/value initialization in certain cases.

struct S {
    int x;
};

S s{}; // s.x == 0

Pros

Essential for safe initialization of scalars, pointers, and static objects.

Cons

You usually do not “choose” zero-initialization directly. It is part of a larger initialization rule.

Use when

Prefer brace/value initialization when you want zero:

int x{};
int* p{};
double d{};

4. Copy initialization

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 directly

Pros

Readable and familiar.

Good when converting from another type:

std::string s = "hello";
double d = 42;

Cons

Does not consider explicit constructors.

struct X {
    explicit X(int) {}
};

X a(1);  // okay
X b{1};  // okay
X c = 1; // error

Allows narrowing conversions unless using braces:

int x = 3.14; // allowed, x == 3, usually warning

Use when

Use when the initialization is conceptually an assignment-like conversion:

std::string name = "David";
auto value = compute();

Avoid when you need explicit constructors.


5. Direct initialization

T x(args);
T x{args};

Parentheses form:

std::string s("hello");
std::vector<int> v(10, 5); // 10 elements, each 5

Brace form is technically direct-list-initialization:

std::string s{"hello"};
std::vector<int> v{1, 2, 3};

Pros

Can call explicit constructors.

struct X {
    explicit X(int) {}
};

X x(42); // okay
X y{42}; // okay

Parentheses avoid some std::initializer_list surprises:

std::vector<int> a(10); // 10 zeros
std::vector<int> b{10}; // one element: 10

Cons

Parentheses can trigger the most vexing parse:

std::string s(); // function declaration, not an object

Braces can prefer std::initializer_list overloads unexpectedly:

std::vector<int> v{10}; // one element, not ten

Use when

Use 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"};

6. List initialization / brace initialization

There are two major forms:

T x{args};    // direct-list-initialization
T x = {args}; // copy-list-initialization

Examples:

int x{42};
int y = {42};

std::vector<int> v{1, 2, 3};

struct Point {
    int x;
    int y;
};

Point p{10, 20};

Important property: prevents narrowing

int a = 3.14; // allowed, usually warning
int b{3.14}; // error

Also:

char c{1000}; // error

Pros

Prevents 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{}; // object

Cons

std::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 20

This can surprise people.

Another example:

std::vector<int> v1(5); // five zeros
std::vector<int> v2{5}; // one element: 5

Use when

Use 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.


7. Aggregate initialization

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 = 0

Arrays are also aggregates:

int values[3]{1, 2, 3};

Pros

Simple, efficient, readable.

Great for data-only types.

struct Config {
    int port;
    bool verbose;
    std::string host;
};

Config c{8080, true, "localhost"};

Cons

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 when

Use for simple value types, configuration structs, DTOs, and POD-like data.


8. Designated initialization, C++20

struct Config {
    std::string host;
    int port;
    bool verbose;
};

Config c{
    .host = "localhost",
    .port = 8080,
    .verbose = true
};

Pros

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};

Cons

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 when

Use for larger aggregate structs where positional initialization is unclear.

Excellent for config objects.


9. Reference initialization

int x = 10;
int& r = x;
const int& cr = 42;
int&& rr = 42;

References must be initialized immediately.

int& ref; // error

A non-const lvalue reference cannot bind to a temporary:

int& r = 42; // error

But a const lvalue reference can:

const int& r = 42; // okay, lifetime extended

Rvalue references bind to rvalues:

std::string&& s = std::string{"hello"};

Pros

Enables aliases, efficient parameter passing, and lifetime extension for const references.

const std::string& name = getName();

Cons

Can create dangling references:

const std::string& bad = getTemporaryString();
// okay only if lifetime is extended directly;
// dangerous in more complex cases

References cannot be reseated.

int a = 1;
int b = 2;
int& r = a;
r = b; // assigns b's value to a; does not rebind r

Use when

Use for function parameters, aliases, and avoiding copies.

void print(const std::string& s);
void mutate(std::vector<int>& v);

10. Member initialization

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_;
};

Pros

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.

Cons

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_
};

Use when

Almost always initialize data members in the constructor initializer list.


11. Default member initializers

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 default

Pros

Keeps defaults near the member declarations.

Excellent for config-like types.

Prevents uninitialized members.

Cons

Can interact subtly with constructors if some constructors override defaults and others do not.

Use when

Use for sensible member defaults.

class SocketOptions {
    int timeoutMs_ = 5000;
    bool reuseAddress_ = true;
};

12. Copy initialization from same type

T b = a;

Example:

std::string a = "hello";
std::string b = a; // copy construction

For move:

std::string c = std::move(a); // move construction

Pros

Readable.

Natural for copying and moving objects.

Cons

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); // okay

Use when

Use when you intentionally want a copy or move.


13. Direct initialization from same type

T b(a);
T b{a};

Example:

std::string a{"hello"};
std::string b(a);
std::string c{a};

Pros

Can be explicit and constructor-like.

Cons

Usually less idiomatic for ordinary copying than:

T b = a;

Use when

Useful in generic code or when you want constructor syntax.


14. Move initialization

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);

Pros

Efficient transfer of resources.

Essential for move-only types like:

std::unique_ptr<T>
std::thread
std::fstream
std::mutex // non-movable, but resource-owning

Cons

The 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 contents

Use when

Use when transferring ownership or avoiding expensive copies.


15. Dynamic initialization with new

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-initialized

For arrays:

int* arr1 = new int[10];   // elements uninitialized
int* arr2 = new int[10]{}; // elements zero-initialized

Pros

Allows dynamic lifetime.

Cons

Manual new/delete is error-prone.

delete p;
delete[] arr;

Leaks, double deletes, and exception-safety problems are easy.

Use when

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++.


16. Initialization with auto

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};    // error

Pros

Avoids repetition.

Good for complex iterator or template types.

auto it = map.find(key);
auto ptr = std::make_unique<Foo>();

Cons

Can deduce unexpected types.

auto s = "hello"; // const char*, not std::string
auto x = 1u; // unsigned int

Use when

Use when the type is obvious or not important:

auto result = compute();
auto socket = makeSocket();

Avoid when the exact type matters for readability or correctness.


17. Class template argument deduction, CTAD

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>

Pros

Reduces verbosity.

std::pair p{"age", 32};

Cons

Can deduce surprising types.

std::vector v{10}; // vector<int> with one element 10

Sometimes explicit type is clearer:

std::vector<int> v(10); // ten ints

Use when

Use when deduction is obvious and improves readability.

Avoid when constructor overloads make meaning ambiguous.


18. Constant initialization

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;

Pros

Avoids static initialization order problems.

No runtime initialization cost.

Good for global constants and low-level systems code.

Cons

Only works when the initializer is a constant expression.

Use when

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};

19. Dynamic initialization of static objects

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.

Pros

Allows complex global objects.

Cons

Can cause the static initialization order fiasco.

Use when

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.


20. Initialization of arrays

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: 3

For std::array:

std::array<int, 3> a{};        // all zero
std::array<int, 3> b{1, 2, 3};

Pros

Brace initialization is clean and safe.

Cons

Raw arrays do not know their size easily and decay to pointers.

Use when

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);

21. Initialization of enums

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-narrowing

Pros

Scoped enums avoid implicit integer conversions.

Cons

Integer initialization of enums can reduce type safety if overused.

Use when

Prefer named enumerators:

Color c = Color::Red;

22. Initialization using std::initializer_list

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};

Pros

Great for list-like types.

Matrix m{
    1, 2, 3,
    4, 5, 6
};

Cons

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 5

Also, 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 const

Use when

Use for list-like APIs.

Avoid adding initializer_list constructors casually because they strongly affect overload resolution.


23. Initialization versus assignment

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.


24. Summary table

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

Practical recommendation

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 clearer

The biggest trap to remember is this:

std::vector<int> a(10); // 10 elements
std::vector<int> b{10}; // 1 element

C++ Initialization — The Complete Guide

Table of Contents

  1. Introduction
  2. What “Initialization” Actually Means
  3. Initialization vs Assignment
  4. The Historical Evolution of Initialization in C++
  5. The Core Categories of Initialization
  6. Default Initialization
  7. Value Initialization
  8. Zero Initialization
  9. Copy Initialization
  10. Direct Initialization
  11. List Initialization
  12. Uniform Initialization
  13. Direct-List Initialization
  14. Copy-List Initialization
  15. Aggregate Initialization
  16. Designated Initialization (C++20)
  17. Reference Initialization
  18. Constant Initialization
  19. Static Initialization
  20. Dynamic Initialization
  21. Ordered vs Unordered Dynamic Initialization
  22. The Static Initialization Order Fiasco
  23. Constructor Member Initialization Lists
  24. Default Member Initializers
  25. Delegating Constructors
  26. Inheriting Constructors
  27. Array Initialization
  28. String Literal Initialization
  29. Enum Initialization
  30. Union Initialization
  31. Bit-field Initialization
  32. POD Initialization
  33. Trivial Initialization
  34. Functional-Style Initialization
  35. Empty Brace Initialization
  36. Narrowing Conversions
  37. std::initializer_list Semantics
  38. Overload Resolution with Brace Initialization
  39. Copy Elision and Guaranteed Copy Elision
  40. Temporary Materialization
  41. Lifetime Extension
  42. Move Initialization
  43. Initialization and Value Categories
  44. Perfect Forwarding and Initialization
  45. CTAD and Deduction-Based Initialization
  46. Auto Initialization Rules
  47. Initialization in Templates
  48. Initialization of constexpr Objects
  49. constinit and consteval
  50. Dynamic Allocation Initialization
  51. Placement New Initialization
  52. Initialization of Atomics
  53. Thread-Local Initialization
  54. Exception Safety During Initialization
  55. Initialization Order Rules
  56. The Most Vexing Parse
  57. Brace Elision
  58. Common Pitfalls
  59. Modern Best Practices
  60. Recommended Style Guide
  61. Summary Cheat Sheet

1. Introduction

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_list selection
  • 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++.


2. What “Initialization” Actually Means

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
  • x begins its lifetime
  • x receives its initial value

Initialization is NOT assignment.


3. Initialization vs 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:

  1. constructs an empty string
  2. later assigns "hello"

This can have different:

  • performance
  • semantics
  • exception behavior

For classes, initialization invokes constructors.

Assignment invokes assignment operators.


4. The Historical Evolution of Initialization in C++

C Era

C mostly had:

  • zero initialization
  • aggregate initialization
  • assignment-style initialization

Example:

int x = 5;
int arr[3] = {1,2,3};

Early C++

C++ introduced:

  • constructors
  • overload resolution
  • references
  • class initialization

Example:

std::string s("hello");

C++11 Revolution

C++11 radically changed initialization with:

  • brace initialization
  • move semantics
  • initializer lists
  • delegating constructors
  • default member initializers

This introduced “uniform initialization”.


C++17 and Beyond

Major additions:

  • guaranteed copy elision
  • CTAD
  • inline variables
  • designated initialization
  • constinit
  • improved constexpr

5. The Core Categories of Initialization

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

6. Default Initialization

Syntax:

T obj;

Examples:

int x;
std::string s;

Behavior depends on type.


Fundamental Types

Local primitives are NOT initialized.

int x;

x has an indeterminate value.

Reading it is undefined behavior.


Class Types

Constructors are called.

std::string s;

Calls default constructor.


Arrays

Each element is default-initialized.

int arr[10];

Each element is uninitialized.


Pros

Efficient.

No unnecessary work.


Cons

Dangerous for primitive types.

Very common source of bugs.


Modern Recommendation

Avoid default-initialized primitive locals unless intentional.

Prefer:

int x{};

7. Value Initialization

Value initialization typically occurs with:

T obj{};
T()

Examples:

int x{};
double d{};
std::string s{};

Primitive Types

Zero-initialized.

int x{}; // 0

Class Types

Default constructor called.


Aggregates

Members recursively value-initialized.

struct Point {
    int x;
    int y;
};

Point p{};

Result:

p.x == 0
p.y == 0

Why Modern C++ Loves {}

This is one of the safest forms of initialization.

int x{};

avoids UB from uninitialized variables.


8. Zero Initialization

Zero initialization sets storage to zero bits conceptually.

Examples:

static int x;
int y{};

Effects:

0
0.0
false
nullptr

Important Note

Zero initialization is usually a stage within another initialization sequence.


9. Copy Initialization

Syntax:

T obj = expr;

Examples:

int x = 5;
std::string s = "hello";

Despite the name, no copy may actually occur.


Important Rule

Copy initialization ignores explicit constructors.

Example:

class X {
public:
    explicit X(int);
};

X a(5);   // OK
X b = 5;  // ERROR

10. Direct Initialization

Syntax:

T obj(args);

Example:

std::string s("hello");

Unlike copy initialization, explicit constructors participate.


Constructor Selection

Direct initialization is often used to precisely select constructors.

Example:

std::vector<int> v(10, 5);

Creates:

  • 10 elements
  • each equal to 5

11. List Initialization

Brace-based initialization:

T obj{args};
T obj = {args};

Introduced in C++11.


12. Uniform Initialization

“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

13. Direct-List Initialization

Syntax:

T obj{args};

Example:

std::vector<int> v{1,2,3};

Explicit constructors ARE considered.


14. Copy-List Initialization

Syntax:

T obj = {args};

Explicit constructors are NOT considered.


15. Aggregate Initialization

Applies to aggregates.

Example:

struct Point {
    int x;
    int y;
};

Point p{1,2};

Members initialize in declaration order.


Missing Members

Missing elements are value-initialized.

Point p{1};

Equivalent to:

x = 1
y = 0

16. Designated Initialization (C++20)

Syntax:

Point p{
    .x = 10,
    .y = 20
};

Benefits

Improves readability.

Prevents ordering mistakes.

Excellent for config structures.


Restrictions

Only for aggregates.

Order must match declaration order.


17. Reference Initialization

References must always be initialized.

int x = 5;
int& r = x;

Const References

Can bind temporaries.

const int& r = 5;

Rvalue References

int&& rr = 5;

18. Constant Initialization

Occurs when an object can be initialized at compile time.

constexpr int x = 5;
constinit int y = 10;

Very important for globals/statics.


19. Static Initialization

Static storage duration objects undergo:

  1. zero initialization
  2. constant initialization if possible

before dynamic initialization.


20. Dynamic Initialization

Initialization requiring runtime code.

Example:

std::string s = compute();

Occurs before main() for globals.


21. Ordered vs Unordered Dynamic Initialization

Within a translation unit:

  • ordered

Across translation units:

  • partially unordered

This creates major problems.


22. The Static Initialization Order Fiasco

Classic C++ issue.

Example:

// file1.cpp
Logger logger;

// file2.cpp
Config config(logger);

Initialization order across files is unspecified.

Can cause crashes.


Solution

Use function-local statics:

Logger& GetLogger() {
    static Logger logger;
    return logger;
}

Thread-safe since C++11.


23. Constructor Member Initialization Lists

Syntax:

class X {
public:
    X(int v)
        : value(v)
    {
    }

private:
    int value;
};

Important

Members initialize in declaration order, NOT initializer-list order.


24. Default Member Initializers

Syntax:

class X {
    int value = 42;
};

Modern and highly recommended.


25. Delegating Constructors

One constructor calls another.

class X {
public:
    X() : X(42) {}

    X(int value) {}
};

26. Inheriting Constructors

class Base {
public:
    Base(int);
};

class Derived : public Base {
public:
    using Base::Base;
};

27. Array Initialization

int arr[3]{1,2,3};

Partial initialization:

int arr[5]{1,2};

Remaining elements become zero.


28. String Literal Initialization

char str[] = "hello";

Includes null terminator.

Equivalent size:

char str[6];

29. Enum Initialization

Scoped enums:

enum class Color {
    Red
};

Color c{Color::Red};

30. Union Initialization

Only one member active at a time.

union U {
    int x;
    float y;
};

U u{42};

Activates x.


31. Bit-field Initialization

struct Flags {
    unsigned a : 1;
    unsigned b : 2;
};

Flags f{1,2};

32. POD Initialization

Historical term.

POD = Plain Old Data.

Modern C++ split POD into:

  • trivial
  • standard-layout

33. Trivial Initialization

Important for low-level systems programming.

Trivial types can often be safely memcpy’d.


34. Functional-Style Initialization

Syntax:

int x(5);

Looks like function-call syntax.


35. Empty Brace Initialization

Very important modern pattern:

T obj{};

Examples:

int x{};
double d{};
int* p{};

Safe and predictable.


36. Narrowing Conversions

Brace initialization forbids narrowing.

Example:

int x{3.14}; // ERROR

This is one of the biggest advantages of braces.


37. std::initializer_list Semantics

Special constructor support:

std::vector<int> v{1,2,3};

Internally uses:

std::initializer_list<int>

38. Overload Resolution with Brace Initialization

Critical topic.

Example:

std::vector<int> a(10,5);
std::vector<int> b{10,5};

Completely different meaning.


Why?

Brace initialization strongly prefers initializer_list constructors.


39. Copy Elision and Guaranteed Copy Elision

Pre-C++17:

T x = T();

Temporary may exist.

Since C++17:

Often no temporary exists at all.


Guaranteed Copy Elision

Example:

T Make() {
    return T{};
}

Returned object constructed directly.

No copy.

No move.


40. Temporary Materialization

Modern standard concept.

Prvalues no longer necessarily create temporary objects immediately.

Temporary objects materialize only when needed.

Advanced but important.


41. Lifetime Extension

Example:

const std::string& r = std::string("hello");

Temporary lifetime extended to match reference lifetime.


42. Move Initialization

Initialization from rvalues.

std::string a = "hello";
std::string b = std::move(a);

Transfers resources efficiently.


43. Initialization and Value Categories

Initialization behavior depends heavily on:

  • lvalues
  • xvalues
  • prvalues

Move semantics fundamentally rely on value categories.


44. Perfect Forwarding and Initialization

Templates often preserve initialization categories:

template<typename T>
void f(T&& value);

Uses forwarding references.


45. CTAD and Deduction-Based Initialization

Class Template Argument Deduction:

std::pair p(1,2.0);

Compiler deduces:

std::pair<int,double>

46. Auto Initialization Rules

auto x = 5;

Deduction depends on initializer.

Brace rules are special:

auto x{5};      // int
auto y = {5};   // initializer_list<int>

47. Initialization in Templates

Templates make initialization extremely complicated because deduction interacts with constructors and overload resolution.


48. Initialization of constexpr Objects

constexpr int x = 5;

Must be initialized with constant expressions.


49. constinit and consteval

constinit

Requires compile-time initialization.

constinit int x = 5;

consteval

Immediate compile-time evaluation.


50. Dynamic Allocation Initialization

new int;
new int();
new int{};

Different semantics.


Important

new int;

Uninitialized.


new int{};

Zero-initialized.


51. Placement New Initialization

Constructs objects in preallocated memory.

new(ptr) T(args);

Critical for allocators and low-level systems code.


52. Initialization of Atomics

std::atomic<int> x{0};

Very important in concurrent programming.


53. Thread-Local Initialization

thread_local int x = 5;

Each thread gets separate instance.


54. Exception Safety During Initialization

If constructor throws:

  • partially constructed members are destroyed
  • fully constructed bases are destroyed

Important RAII guarantee.


55. Initialization Order Rules

Critical rule:

Members initialize in declaration order.

NOT constructor list order.


56. The Most Vexing Parse

Classic syntax ambiguity.

std::string s();

Declares function.

Not object.

Brace initialization helps avoid this.


57. Brace Elision

Nested braces may be omitted.

int arr[2][2] = {
    1,2,
    3,4
};

58. Common Pitfalls

Uninitialized Variables

int x;

initializer_list Surprises

std::vector<int> v{10};

One element.

NOT ten elements.


Static Initialization Order

Global object dependencies are dangerous.


Dangling References

const std::string& r = GetString();

Can be dangerous.


Narrowing

int x{3.14}; // error

59. Modern Best Practices

Prefer Brace Initialization

int x{};
std::string s{};

Prefer Default Member Initializers

class X {
    int value = 0;
};

Avoid Raw Uninitialized Variables

Especially primitives.


Prefer Function-Local Statics

Avoid global initialization order issues.


Be Careful with std::initializer_list

Especially containers.


60. Recommended Style Guide

Good General Rules

Use {} by default

T obj{};

Use () when constructor selection matters

std::vector<int> v(10,5);

Avoid naked primitive locals

Bad:

int x;

Good:

int x{};

Prefer RAII

Avoid manual initialization management.


61. Summary Cheat Sheet

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

Final Thoughts

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++.


int i = 5; vs int i{5}; vs int i(5); — Which Initialization Style Is Better?

For this exact case:

int i = 5;
int i{5};
int i(5);

all three create the same object:

i == 5

For 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.


1. int i = 5;

This is copy initialization.

int i = 5;

Despite the name, for int there is no “copy” happening. The value 5 initializes i directly.

Arguments in favor

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.”

Arguments against

The biggest weakness is that it allows narrowing conversions.

int i = 5.7; // allowed, i becomes 5

Usually the compiler warns, but it is not ill-formed by default.

By contrast:

int i{5.7}; // error

So 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; // ERROR

That 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 assignment

This can confuse beginners, though most experienced C++ programmers are used to it.

Best use cases

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;

2. int i{5};

This is direct-list-initialization, also commonly associated with uniform initialization.

int i{5};

Arguments in favor

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}; // error

This is a real advantage. Many C++ bugs come from silent narrowing:

double price = 19.99;
int cents = price; // silently becomes 19

With braces:

int cents{price}; // error

That 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{}; // object

For 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.

Arguments against

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 5

And:

std::vector<int> a(10, 20); // ten elements, each 20
std::vector<int> b{10, 20}; // two elements: 10 and 20

So 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.

Best use cases

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};

3. int i(5);

This is direct initialization using parentheses.

int i(5);

For int, it works perfectly.

Arguments in favor

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.

Arguments against

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 5

So it is less safe than:

int i{5.7}; // error

Another 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.

Best use cases

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.


Direct Comparison

With exact literal 5

All equivalent:

int a = 5;
int b{5};
int c(5);

No meaningful runtime difference.

The compiler will produce the same machine code.

With narrowing

Different:

int a = 5.7; // allowed
int b{5.7}; // error
int c(5.7); // allowed

This is the strongest argument for braces.

With readability

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.

With consistency

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();

My Ranking for int i = 5;, int i{5};, int i(5);

For this exact case, I would rank them:

1. Best default modern choice

int i{5};

Reason:

  • prevents narrowing,
  • clearly initializes,
  • works well with modern C++ style,
  • no performance downside,
  • avoids assignment-looking syntax.

2. Also excellent and very readable

int i = 5;

Reason:

  • extremely readable,
  • familiar,
  • idiomatic,
  • especially good for simple scalar values.

Weakness:

  • allows narrowing.

3. Valid but least preferable for primitive variables

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.

Practical Style Recommendation

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);

Important Exception: auto

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>

Important Exception: Constructors with Different Meanings

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.


Final Verdict

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 safety

Initialization 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.


The One-Sentence Summary

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.


Why C Initialization Is So Simple

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's Design Goal

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.


What C Sacrifices

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


Why C++ Became More Complicated

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.


The Central Problem C++ Had to Solve

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.


C++ Treats Types as Peers

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.


Why C++ Has Multiple Initialization Forms

C++ is trying to satisfy conflicting goals.

It wants:

C compatibility

int x = 5;

Constructor support

std::string s("hello");

Generic programming

T obj(args);

Safety

int x{5};

Aggregates

Point p{1, 2};

Containers

std::vector<int> v{1, 2, 3};

Each requirement pushed the language toward additional initialization forms.


Why Uniform Initialization Was Added

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.


Why Uniform Initialization Didn't Fully Succeed

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.


C++ Optimizes for Flexibility

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 Takes a Completely Different Approach

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 Core Principle

Rust's philosophy is:

Invalid states should be difficult or impossible to represent.

Initialization is part of that.


Rust Does Not Allow Uninitialized Variables

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.


Rust Does Not Need Constructor Syntax

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.


Why Rust Avoids Constructor Overloading

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.


Rust Prioritizes Readability Over Flexibility

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.


Rust Uses the Type System to Enforce Initialization

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.


Comparing the Three Philosophies

C

Primary goal:

Control and simplicity

Trusts programmer.

Allows dangerous operations.

Minimal language complexity.


C++

Primary goal:

Zero-cost abstraction

Supports both:

int

and

std::vector<std::unique_ptr<MyType>>

using a unified language.

Accepts complexity to achieve flexibility.


Rust

Primary goal:

Safety through the type system

Restricts programmer freedom.

Uses compiler checks aggressively.

Prefers explicit APIs.


Why C++ Allows More Initialization Forms Than Rust

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.


Why C Still Has the Simplest Model

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.


The Cost of Each Philosophy

C

Advantages:

  • Simple
  • Predictable
  • Tiny language surface

Disadvantages:

  • Easy to make mistakes
  • Uninitialized memory common
  • Weak type safety

C++

Advantages:

  • Extremely expressive
  • Supports sophisticated abstractions
  • Zero-cost abstractions
  • Powerful generic programming

Disadvantages:

  • Complex rules
  • Multiple initialization forms
  • Overload resolution surprises
  • Steep learning curve

Rust

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 Difference

The deepest philosophical difference is this:

C

The programmer is responsible for correctness.

C++

The programmer is responsible for correctness, but the language should provide powerful abstractions.

Rust

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.


The Short Version

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.


What Is Uniform Initialization?

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.


Why It's Called "Uniform"

Because the same syntax works for:

Primitive types

int x{5};
double d{3.14};

Class types

std::string s{"hello"};

Containers

std::vector<int> v{1, 2, 3};

Aggregates

struct Point {
    int x;
    int y;
};

Point p{1, 2};

Arrays

int arr[]{1, 2, 3};

The syntax is "uniform."


The Problem: Uniform Initialization Is Not a Standard Term

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.


What Is Direct-List-Initialization?

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.


What Is Copy-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.


Uniform Initialization Includes Both

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-init

Both are brace-based initialization styles.


Why Direct-List-Initialization Matters

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.


Example: Direct-List vs Copy-List

class Widget {
public:
    explicit Widget(int) {}
};

Direct-list:

Widget w{42};

OK.


Copy-list:

Widget w = {42};

Error.

The constructor is explicit.


Uniform Initialization Doesn't Tell You Which Rule Applies

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.


Another Example

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)

Why Uniform Initialization Isn't Truly Uniform

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.


Relationship Between the Terms

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.


Which Term Should You Use?

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.


The Practical Rule

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."


Why Copy-List-Initialization Is Preferred Over Direct-List-Initialization for auto in C++

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.

Two Forms of List Initialization

C++ provides two forms of list initialization:

Copy-list-initialization

auto x = {1, 2, 3};

Direct-list-initialization

auto x{1, 2, 3};

Although the only visible difference is the presence of =, the language treats them differently when auto is involved.

Copy-List-Initialization and std::initializer_list

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.

Direct-List-Initialization in Modern C++

Since C++17, direct-list-initialization follows different rules.

A single element is deduced as its ordinary type:

auto x{42};  // int

Multiple elements are not allowed:

auto x{1, 2, 3}; // error

The 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.

Why the Change Was Made

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.

Behavior Before C++17

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.

The C++17 Fix

C++17 changed the rules to distinguish the two forms clearly.

Copy-list-initialization

auto x = {42};      // std::initializer_list<int>
auto y = {1, 2, 3}; // std::initializer_list<int>

Direct-list-initialization

auto x{42};         // int
auto y{1, 2, 3};    // error

The new rule makes direct-list-initialization behave much more like ordinary type deduction.

A Rare Source Compatibility Change

Because of the C++17 change, the exact same code can have different meanings depending on the language version.

Consider:

auto i{42};

C++11/C++14

std::initializer_list<int>

C++17 and Later

int

This 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

Why Copy-List-Initialization Is Preferred

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_list in 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 a std::initializer_list.
  • Use auto x{expr}; when you want ordinary type deduction from a single expression.

Summary

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 deduction

and

auto x{42};         // ordinary type deduction

This distinction makes auto more predictable, reduces surprises when refactoring, and aligns direct-list-initialization more closely with the behavior of explicitly typed variables.


Recommended Initialization Style for Professional C++11+ Development

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.


1. Use {} for scalar values

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, bad

For professional code, this is a strong advantage.


2. Use {} for zero/default initialization

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.


3. Use {} for aggregates and simple structs

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.


4. Use constructor member initializer lists for class members

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.


5. Use default member initializers

Prefer:

class SocketOptions {
private:
    int timeoutMs_{5000};
    bool reuseAddress_{true};
    bool nonBlocking_{false};
};

This prevents forgotten initialization and keeps defaults near the data members.


6. Use = with auto

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.


7. Use () when constructor overload selection matters

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 0

Braces strongly prefer std::initializer_list constructors.

So for containers, be intentional.


8. Use = when it improves readability

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.


My Practical Rule Set

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.


Final Recommendation

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 = with auto, prefer parentheses when selecting constructors, and never follow “always use one form” blindly.


Uniform Initialization: A Noble Goal That C++ Never Fully Achieved

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.


Why the Goal Was Attractive

A truly uniform initialization syntax would have several benefits:

Fewer rules

Instead of teaching:

=
()
{}

you could simply teach:

{}

More consistency

Every type would look the same:

int x{5};
std::string s{"hello"};
Widget w{arg};

Better safety

Brace initialization rejects narrowing:

int x{3.14}; // error

which is usually desirable.

No most-vexing-parse

std::string s();

is a function declaration.

std::string s{};

is clearly an object.


Why It Failed

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.


The Famous std::vector Example

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.


The Constructor Selection Problem

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.


The auto Problem

Another example:

auto x = 5;

deduces:

int

But:

auto x = {5};

deduces:

std::initializer_list<int>

A supposedly "uniform" syntax suddenly produces a different type.


The Irony

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.


What Actually Happened

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";     // readability

This 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.


The Real Lesson

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment