How to Initialize C++ Variables

C++ Variable Initialization

In C++, initializing a variable means providing it with an initial value at the moment of its creation (declaration). This is different from assignment, which replaces the value of an already existing object.

Initialization Methods

FormNameWhat happens
int a;Default initializationFor local variables of fundamental (built-in) types (int, double, pointers), it does not define an initial value (it starts with an indeterminate value). Reading a before assigning a value causes undefined behavior. (Note: global or static variables are zero-initialized).
int b = 5;Copy initializationInitializes b with 5. Although = is usually used for assignment, here it is part of the declaration. It performs implicit narrowing conversions (e.g., 6.4 is truncated to 6).
int c(6);Direct initializationInitializes c with 6 using parentheses. For classes, it directly invokes the matching constructor. It also performs implicit narrowing conversions (e.g., 6.4 is truncated to 6).
int d{7};Direct-list initializationInitializes d with 7 (introduced in C++11 as uniform initialization). It disallows narrowing conversions, causing a compilation error if you try to initialize an int with 6.4.
int d = {7};Copy-list initializationSimilar to int d{7}; (narrowing conversions are prohibited), but it does not consider constructors marked as explicit.
int e{};Value initializationFor fundamental types like int, it performs zero-initialization (initializes with 0). For classes, it calls the default constructor.

Comparison and When to Use Each Form

Although several syntaxes appear to do the same thing for primitive types, each has an ideal purpose and context in modern C++:

1. List / Uniform Initialization (int d{7}; and int e{};) — The Default Choice

  • Comparison: It is the most modern (introduced in C++11), safe, and consistent form. It works for any type (fundamental types, aggregates, structs, classes, and containers) and prohibits data-loss conversions (narrowing conversions).
  • When to use:
    • Almost always as the default in modern C++ code.
    • Use Type var{value}; when you already know the initial value and want protection against accidental data truncation.
    • Use Type var{}; when you want to initialize the variable with a safe default value (0, nullptr, false, or the default constructor), avoiding garbage values in memory and ambiguity with function declarations (Most Vexing Parse).

2. Copy Initialization (int b = 5;) — Traditional and Readable

  • Comparison: Inherited from C, it is the most familiar syntax. However, it is less restrictive (allows silent narrowing on fundamental types) and does not work with constructors marked as explicit.
  • When to use:
    • With simple scalar types (int, bool, double) when the team or codebase prefers the traditional readability of =.
    • When using type deduction with auto (e.g., auto x = 5; or AAA style — Almost Always Auto), where the type is already determined directly by the right-hand side expression.

3. Direct Initialization with Parentheses (int c(6);) — Specific Constructor Cases

  • Comparison: Calls constructors directly (including explicit ones), but allows narrowing conversions and suffers from the Most Vexing Parse if used without arguments (Type c();).
  • When to use:
    • When a class has an overload for std::initializer_list and you want to call a different specific constructor instead of the list one. The classic example is std::vector<int> v(5, 10); (creates 5 elements with value 10), instead of std::vector<int> v{5, 10}; (creates 2 elements: 5 and 10).
    • Inside forwarding functions or generic factories in legacy code prior to C++20.

4. Copy-List Initialization (int d = {7};) — Aggregates and Function Returns

  • Comparison: Combines the look of = with the safety of braces {} against narrowing, but does not allow calling explicit constructors.
  • When to use:
    • When clearly initializing aggregate structs or arrays: Point p = {10, 20};.
    • When you want to make it explicit to both the reader and the compiler that no explicit conversion should occur.

5. Default Initialization (int a;) — Avoid, Except for Performance or Immediate Input

  • Comparison: It is the only form that leaves local primitive types with garbage memory (indeterminate value). For classes with a default constructor (such as std::string s;), it calls the constructor normally, but for fundamental types it is a common source of bugs.
  • When to use:
    • Avoid by default.
    • Use only when the variable will be immediately populated on the very next line by an input or read function (e.g., int x; std::cin >> x;).
    • In performance-critical scenarios (e.g., large buffers or numeric arrays) where the cost of zeroing memory right before overwriting it is demonstrably unnecessary.

Quick Decision Summary

SituationRecommended FormExample
Known initial value (general rule)Direct-list {value}int age{25};
Needs to start zeroed / emptyValue {}int counter{}; / int* ptr{};
Constructor conflicts with initializer_listDirect (args)std::vector<int> v(100, 0);
Using with autoCopy = or {}auto total = computeTotal();
Populated via std::cin or out-paramValue {} (or default if immediate)int input{}; std::cin >> input;

Key Differences and Pitfalls

For simple types like int, forms b, c, and d yield the same final value. However, the differences become critical in four scenarios:

1. Narrowing Conversions

Brace initialization {} prevents implicit conversions that could lose data at compile time:

int x1 = 3.5;  // Compiles (with warning): truncates value to 3
int x2(3.5);   // Compiles (with warning): truncates value to 3
int x3{3.5};   // Compilation ERROR: narrowing conversion from double to int

2. explicit Constructors

In user-defined types (class / struct), copy initialization (=) does not accept constructors marked as explicit, whereas direct initialization (() or {}) does:

struct Box {
    explicit Box(int size) {}
};

Box b1(10);       // OK: direct initialization
Box b2{10};       // OK: direct-list initialization
// Box b3 = 10;   // ERROR: constructor is explicit
// Box b4 = {10}; // ERROR: copy-list initialization does not accept explicit

3. Most Vexing Parse (The Empty Parentheses Problem)

Attempting to initialize an object with empty parentheses is parsed by the compiler as a function declaration, not a variable definition:

int f();   // Declares a function named 'f' returning int (does NOT create a variable!)
int g{};   // Creates a variable 'g' initialized to 0

4. Watch Out for std::initializer_list

When using braces {} with standard library types that have a constructor taking std::initializer_list (such as std::vector), the compiler strongly prefers that constructor:

#include <vector>

std::vector<int> v1(5, 10); // Creates a vector with 5 elements, all of value 10
std::vector<int> v2{5, 10}; // Creates a vector with 2 elements: {5, 10}

Best Practices in Modern C++

General Rule (C++ Core Guidelines — ES.23): Prefer the {}-initializer syntax (list-initialization).

  • When you have an initial value: use int x{value}; — prevents accidental conversions with loss of precision.
  • When you want a default/zero value: use int x{}; — guarantees the variable starts zeroed without the risk of garbage memory or the Most Vexing Parse.
  • When using auto: prefer auto x = value; or auto x{value}; (with a single element since C++17, auto x{5}; deduces int, whereas auto x = {5}; deduces std::initializer_list<int>).

References

https://www.learncpp.com/cpp-tutorial/variable-assignment-and-initialization/ https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#es20-always-initialize-an-object