Default initialization and some words regarding the complexity of initialization in C++
To limit the scope of this discussion, let T
be any kind of type (fundamental such as int
, class types, aggregate as well as non-aggregate), and the t
be a variable of automatic storage duration:
int main() {
T t; // default initialization
}
The declaration of t
means t
will be initialized by means of default initialization. Default initialization acts differently for different kind of types:
- For fundamental types such as
int
, bool
, float
and so on, the effect is that t
is left in an uninitialized state, and reading from it before explicitly initializing it (later) is undefined behavior
- For class types, overload resolution resolve to a default constructor (which may be implicitly or implicitly generated), which will initialize the object but where its data member object could end up in an uninitialized state, depending on the definition of the default constructor selected
- For array types, every element of the array is default-initialize, following the rules above
C++ initialization is complex, and there are many special rules and gotchas that can end up with uninitialized variable or data members of variables whence read results in UB.
Hence a long-standing recommendation is to always explicitly initialized you variables (with automatic storage duration), and not rely on the fact that default initialization may or may not result in a fully initialized variable. A common approach is (attempting) to use value initialization, by means of initialization of variable with empty braces:
int main() {
int i{}; // value-initialization -> zero-initialization
SomeAggregateClass ac{}; // aggregate initialization
}
However even this approach can fail for class types if one is not careful whether the class is an aggregate or not:
struct A {
A() = default; // not user-provided.
int a;
};
struct B {
B(); // user-provided.
int b;
};
// Out of line definition: a user-provided
// explicitly-defaulted constructor.
B::B() = default;
In this example (in C++11 through C++17), A
is an aggregate, whereas B
is not. This, in turn, means that initialization of B
by means of an empty direct-list-init will result in its data member b
being left in an uninitialized state. For A
, however, the same initialization syntax will result in (via aggregate initialization of the A
object and subsequent value initalization of its data member a) zero-initialization of its data member a
:
A a{};
// Empty brace direct-list-init:
// -> A has no user-provided constructor
// -> aggregate initialization
// -> data member 'a' is value-initialized
// -> data member 'a' is zero-initialized
B b{};
// Empty brace direct-list-init:
// -> B has a user-provided constructor
// -> value-initialization
// -> default-initialization
// -> the explicitly-defaulted constructor will
// not initialize the data member 'b'
// -> data member 'b' is left in an unititialized state
This may come as a surprise, and with the obvious risk of reading the uninitialized data member b
with the result of undefined behaviour:
A a{};
B b{}; // may appear as a sound and complete initialization of 'b'.
a.a = b.b; // reading uninitialized 'b.b': undefined behaviour.