Overview of C++ Variable Initialization

C++ variable initialization is quite complex, with several distinct categories that differ in behavior. The following list attempts to make sense of it.

Default initialization

Default initialization applies when no initializer is specified at all, or when a class member is omitted from the member initialization list.

Primitive types remain uninitialized!

T t;
new T;

Value initialization

Initialization to a default value. For class-types, this is essentially identical to default initialization, but primitive types are zero-initialized.

T t{};
T(); T{};
new T(); new T{};
: member(), member{}     // class member initializer lists

Direct initialization

Direct initialization applies when initializing objects without explicit assignment. It also is also used for explicit casts, whether function-style or via static_cast and applies to Lambda closure arguments captured by value (which may be regarded as a special case of member initialization).

During direct initialization, both explicit and non-explicit constructors are considered.

T object(arg, ... );
T(arg1, arg2, ... );
new T(args, ...)
: member(args, ...)      // class member initializer lists
T(other)                 // function-style cast
static_cast<T>(other)    // explicit static_cast
[arg](){...}             // lambda closure arguments captured by value

Copy initialization

Copy initialization is used during initialization-by-assignment and whenever objects are implicitly copied, for example during pass-by-value, return-by-value, and catch-by-value, and when throwing exceptions. In the last case, it is used to copy the thrown object into the magic place where exceptions live during stack unwind.

During copy initialization, only non-explicit constructors are considered. This is the fundamental difference to direct initialization.

T object = other;        // Initialization via assignment
T array[N] = {other};    // In array-initialization, the individual
                         // values are copy-initialized
f(other)                 // Pass-by-value
return other;            // Return-by-value
catch (T object)         // Catch-by-value
throw object;

List initialization

List initialization applies wherever braced initializer lists are used. The exact rules are quite complicated, but in essence list initialization mirrors the behavior of value-, direct- or copy-initialization (depending on context) with the added restriction that narrowing conversions are not allowed.

  • Value list initialization

    T object{};
    T{};
    new T{}
    Class { T member{}; };
    : member{}                         // Class member initializer lists
    
  • Direct list initialization

    T object{arg, ...};
    T {arg, ...};
    new T{arg, ...}
    Class { T member{arg, ...}; };     // Class member default initializer
    : member{arg, ...}                 // Class member initializer lists
    
  • Copy list initialization

    T object = {arg, ...};
    object = {arg, ...};
    Class { T member = {arg, ...}; };  // Class member default initializer
    function({arg, ...});              // Initializes temporary for the function arg
    return {arg, ...};                 // Initializes temporary for return value
    

Aggregate initialization

Aggregate initialization is a special case of list initialization. It is used when initializing arrays or simple structs (all-members-public, no user-provided c'tors, ...; see here for details).

It applies irrespective of whether the assignment or non-assignment form of initialization is used.

T object = {arg1, arg2, ...};   // If T is an array or a simple struct
T object{arg1, arg2, ...};      // If T is an array or a simple struct

Reference initialization

Used to initialize references.

Interesting tidbit: references can be bound to temporaries and the temporary lifetime is extended to the reference lifetime except for:

  • returning references to local variables (produces dangling references)
  • temporaries used as pass-by-reference arguments in function calls or new expressions (temporary lifetime extended until the end of the current expression)

For example, function-local references can be initialized with temporaries and will remain valid until function exit.