Please consider this short code example:
#include <iostream>
struct A
{
A() { std::cout << "A() "; }
~A() { std::cout << "~A() "; }
};
struct B { const A &a; };
struct C { const A &a = {}; };
int main()
{
B b({});
std::cout << ". ";
C c({});
std::cout << ". ";
}
GCC prints here ( https://gcc.godbolt.org/z/czWrq8G5j )
A() ~A() . A() . ~A()
meaning that the lifetime of A
-object initializing reference in b
is short, but in c
the lifetime is prolonged till the end of the scope.
The only difference between structs B
and C
is in default member initializer, which is unused in main(), still the behavior is distinct. Could you please explain why?
When we define a struct (or class) type, we can provide a default initialization value for each member as part of the type definition. This process is called non-static member initialization, and the initialization value is called a default member initializer.
If no default member initializer exists, the member remains uninitialized. Members are always initialized in the order of declaration. The case we want to watch out for is s1.x. Because s1 has no initializer list and x has no default member initializer, s1.x remains uninitialized (which is bad, since we should always initialize our variables).
Modern C++ Features – Default Initializers for Member Variables. One of the less discussed but nevertheless useful features in C++11 is the possibility to provide initializers for class members right in the class definition. How it works. You can simply provide a default value by writing an initializer after its declaration in the class definition.
If an explicit initialization value exists, that explicit value is used. If an initializer is missing and a default member initializer exists, the default is used. If an initializer is missing and no default member initializer exists, value initialization occurs. If a default member initializer exists, the default is used.
C c(...);
is syntax for direct initialisation. Overload resolution would find a match from the constructors of C
: The move constructor can be called by temporary materialisation of a C
from {}
. {}
is value initialisation which will use the default member initialiser. Thus, the default member initialiser isn't unused. Since C++17, the move constructor isn't necessary and {}
initialises variable c
directly; In this case c.a
is bound directly to the temporary A
and the lifetime is extended until destruction of C
.
B
isn't default constructible, so the overload resolution won't find a match. Instead, aggregate initialisation is used since C++20 - prior to that it would be ill-formed. The design of the C++20 feature is to not change behaviour of previously valid programs, so aggregate initialisation has lower priority than the move constructor.
Unlike in the case of C
, the lifetime of the temporary A
isn't extended because parenthesised initialisation list is an exceptional case. It would be extended if you used curly braces:
B b{{}};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With