Initialization of Variables in C++

When an object is defined, you can optionally provide an initial value for the object. The process of specifying an initial value for an object is called initialization, and the syntax used to initialize an object is called an initializer. Informally, the initial value is often called an initializer as well.


There are 5 common forms of initialization in C++:

int a;         // default-initialization (no initializer)

// Traditional initialization forms:
int b = 5;     // copy-initialization (initial value after equals sign)
int c ( 6 );   // direct-initialization (initial value in parenthesis)

// Modern initialization forms (preferred):
int d { 7 };   // direct-list-initialization (initial value in braces)
int e {};      // value-initialization (empty braces)

You may see the above forms written with different spacing (e.g. int b=5; int c(6);, int d{7};, int e{};). Whether you use extra spaces for readability or not is a matter of personal preference.

As of C++17, copy-initialization, direct-initialization, and direct-list-initialization behave identically in most cases. We'll cover the most relevant case where they differ below.

Default-initialization

When no initializer is provided (such as for variable a above), this is called default-initialization. In many cases, default-initialization performs no initialization, and leaves the variable with an indeterminate value (a value that is not predictable, sometimes called a garbage value).

Copy-initialization

When an initial value is provided after an equals sign, this is called copy-initialization. This form of initialization was inherited from the C language.

int width = 5; // copy-initialization of value 5 into variable width

Much like copy-assignment, this copies the value on the right-hand side of the equals into the variable being created on the left-hand side. In the above snippet, variable width will be initialized with value 5.

Copy-initialization had fallen out of favor in modern C++ due to being less efficient than other forms of initialization for some complex types. However, C++17 remedied the bulk of these issues, and copy-initialization is now finding new advocates. You will also find it used in older code (especially code ported from C), or by developers who simply think it looks more natural and is easier to read.

Direct-initialization

When an initial value is provided inside parenthesis, this is called direct-initialization.

int width ( 5 ); // direct initialization of value 5 into variable width

Direct-initialization was initially introduced to allow for more efficient initialization of complex objects (those with class types). Just like copy-initialization, direct-initialization had fallen out of favor in modern C++, largely due to being superseded by direct-list-initialization. However, direct-list-initialization has a few quirks of its own, and so direct-initialization is once again finding use in certain cases.

List-initialization

The modern way to initialize objects in C++ is to use a form of initialization that makes use of curly braces. This is called list-initialization (or uniform initialization or brace initialization).

List-initialization comes in two forms:

int width { 5 };    // direct-list-initialization of initial value 5 into variable width (preferred)
int height = { 6 }; // copy-list-initialization of initial value 6 into variable height (rarely used)

Prior to C++11, some types of initialization required using copy-initialization, and other types of initialization required using direct-initialization. Copy-initialization can be hard to differentiate from copy-assignment (because both use an =). And direct-initialization can be difficult to differentiate from function-related operations (because both use parentheses).

List-initialization was introduced to provide a initialization syntax that works in almost all cases, behaves consistently, and has an unambiguous syntax that makes it easy to tell where we're initializing an object.

Additionally, list-initialization also provides a way to initialize objects with a list of values rather than a single value (which is why it is called list-initialization).

Value-initialization and zero-initialization

When a variable is initialized using an empty set of braces, a special form of list-initialization called value-initialization takes place. In most cases, value-initialization will implicitly initialize the variable to zero (or whatever value is closest to zero for a given type). In cases where zeroing occurs, this is called zero-initialization.

int width {}; // value-initialization / zero-initialization to value 0

Some Advantages of Universal/Uniform Initialization (Curly Braces {})

Using the {} allow us to guarantee that the variable will be initialized. If, for instance, you have an empty {} such as int amount{}, the compiler will initialize the variable amount to its type default value, in this case a 0 (zero) for int.

The use of an un-initialized variable can cause an undefined behavior because we have no way to know the value of the variable when the program starts. All calculations based on variables not initialized can't be trusted. When a variable is not initialized, for instance, int count; the compiler will allocate a block of memory that can hold an int (4 bytes). However we don't know what value is present in that block of memory. It could be 0 or it could be any int value.


Another advantage of using {} is to avoid narrowing your values. You can always fit a smaller value in a larger one. For instance, you can fit a char in a double, but the opposite may sometimes be possible and this can give you problems.

Imagine that you want to create a variable to hold a double value, but by mistake (you are human) you code an int variable and initialize it with a double value using = (see code below). Because of the =, the compiler will narrow down your double value in order to make it fit inside your int variable. However, this may not be what you wanted. This problem can be avoided and the compiler can help you to see the issue. Let's see the example below:

int cost_1 = 19.99; //it will narrow down to 19
int cost_2{29.99}; //the compiler will issue an error

In the first example, we used an int variable to hold a double value. In this case, depending on your compiler or how you configured it, the compiler will narrow down the 19.99 to 19. If you don't catch that mistake, your calculation could be wrong and you may spend a long time trying to figure out where the bug is. However, if you use the second example with the {}, the compiler will give you an error message saying: error: type 'double' cannot be narrowed to 'int' in initializer list and will not compile. With that you can easily spot your mistake and correct it.

Unused Initialized Variables

Modern compilers will typically generate warnings if a variable is initialized but not used (since this is rarely desirable). And if treat warnings as errors is enabled, these warnings will be promoted to errors and cause the compilation to fail.

Consider the following innocent looking program:

int main()
{
    int x { 5 }; // variable x defined

    // but not used anywhere

    return 0;
}

When compiling this with GCC and “treat warnings as errors” on, the following error is generated:

prog.cc: In function 'int main()':
prog.cc:3:9: error: unused variable 'x' [-Werror=unused-variable]

and the program fails to compile.


There are a few easy ways to fix this.

  1. If the variable really is unused and you don't need it, then the easiest option is to remove the definition of x (or comment it out). After all, if it's not used, then removing it won't affect anything.
  2. Another option is to simply use the variable somewhere

The [[maybe_unused]] attribute C++17

In some cases, neither of the above options are desirable. Consider the case where we have a set of math/physics values that we use in many different programs:

#include <iostream>

int main()
{
    // Here's some math/physics values that we copy-pasted from elsewhere
    double pi { 3.14159 };
    double gravity { 9.8 };
    double phi { 1.61803 };

    std::cout << pi << '\n';  // pi is used
    std::cout << phi << '\n'; // phi is used

    // The compiler will likely complain about gravity being defined but unused

    return 0;
}

If we use these values a lot, we probably have these saved somewhere and copy/paste/import them all together.

However, in any program where we don't use all of these values, the compiler will likely complain about each variable that isn't actually used. In the above example, we could easily just remove the definition of gravity. But what if there were 20 or 30 variables instead of 3? And what if we use them in multiple places? Going through the list of variables to remove/comment out the unused ones takes time and energy. And later if we need one that we've previously removed, we'll have to spend more time and energy to go back and re-add/uncomment it.

To address such cases, C++17 introduced the [[maybe_unused]] attribute, which allows us to tell the compiler that we're okay with a variable being unused. The compiler will not generate unused variable warnings for such variables.

The following program should generate no warnings/errors:

#include <iostream>

int main()
{
    [[maybe_unused]] double pi { 3.14159 };  // Don't complain if pi is unused
    [[maybe_unused]] double gravity { 9.8 }; // Don't complain if gravity is unused
    [[maybe_unused]] double phi { 1.61803 }; // Don't complain if phi is unused

    std::cout << pi << '\n';
    std::cout << phi << '\n';

    // The compiler will no longer warn about gravity not being used

    return 0;
}

Additionally, the compiler will probably optimize these variables out of the program, so they have no performance impact.

The [[maybe_unused]] attribute should only be applied selectively to variables that have a specific and legitimate reason for being unused (e.g. because you need a list of named values, but which specific values are actually used in a given program may vary). Otherwise, unused variables should be removed from the program.