ead ead - 1 year ago 50
C++ Question

Lifetime of temporary objects during list-initialization

I always assumed, that temporary objects live until the end of a full-expression. Here is however a curious difference between initializations of a

and an array.

Please consider the following code:

#include <iostream>
#include <vector>

struct ID{
static int cnt;
// the number of living object of class ID at the moment of creation:
int id;



int ID::cnt=0;

int main(){

int arr[]{ID().id, ID().id};
std::vector<int> vec{ID().id, ID().id};

std::cout<<" Array: "<<arr[0]<<", "<<arr[1]<<"\n";
std::cout<<" Vector: "<<vec[0]<<", "<<vec[1]<<"\n";

The output of this program is a little bit (at least for me) unexpected:

Array: 1, 1
Vector: 1, 2

That means, the temporary objects are alive during the whole initialization of the
but they are created and destructed one after each other in the case of an array. I would expect the temporaries to live until the full-expression
int arr[]{ID().id, ID().id};
is completed.

The standard mentions one exception concerning the lifetime of temporary objects and initialization of arrays (12.2). However I don't get its meaning and don't know why it is applied in this particular case:

There are two contexts in which temporaries are destroyed at a
different point than the end of the full-expression. The first context
is when a default constructor is called to initialize an element of an
array. If the constructor has one or more default arguments, the
destruction of every temporary created in a default argument is
sequenced before the construction of the next array element, if any.

Overview of the results with different compilers (MSVS result is a curtesy of NathanOliver):

Array Vector
clang 3.8 1, 2 1, 2
g++ 6.1 1, 1 1, 2
icpc 16 1, 1 1, 2
MSVS 2015 1, 1 1, 2

As ecatmur pointed out, for aggregate initialization every element of the braced-init-list is a full-expression, thus the following code

struct S{
int a;
int b;
} s{ID().id, ID().id};
std::cout<<" Struct: "<<s.a<<", "<<s.b<<"\n";

should print
Struct 1, 1
to the console. That is exactly what the program compiled by g++ does. However, clang seems to have a bug - the resulting program prints
Struct 1, 2

Answer Source

The correct output according to the rules of the Standard is 1, 1 for the array and 1, 2 for the vector; this is because constructing a vector (including from a braced-init-list) requires a call to a constructor while constructing an array does not.

The language that governs this is in [intro.execution]:

10 - A full-expression is an expression that is not a subexpression of another expression. [...] If a language construct is defined to produce an implicit call of a function, a use of the language construct is considered to be an expression for the purposes of this definition. [...]

This is fine as a top-level overview, but it leaves unanswered some questions:

  • Precisely which language construct counts as the construct producing an implicit call of a function;
  • What actually counts as an implicit call of a function; presumably a call to a user-defined constructor is a call of a function, but what about a constructor that is defaulted or defined as defaulted?

An array is an aggregate so is initialized from a braced-init-list according to [dcl.init.aggr]; this says that each element is initialized directly from the corresponding element of the list, so there is no implicit function call (at least not corresponding to the overall initialization). At a syntax level, within an initializer ([dcl.init]/1) using a braced-init-list as the brace-or-equal-initializer, the full-expressions are the expressions contained within braces and separated by commas. At the end of each full-expression, the destructors of temporaries are required to run as none of the three contexts mentioned in [class.temporary] are the case here.

The case for the initialization of a vector is different, since you are using the initializer_list constructor, so an implicit call of a function (i.e. the initializer_list constructor) occurs; this means that there is an implicit full-expression surrounding the whole initialization, so the temporaries are destroyed only when the initialization of the vector completes.

Confusingly, [dcl.init.list] says that your code is "roughly equivalent" to:

const int __a[2] = {int{ID().id}, int{ID().id}};  // #1
std::vector<int> vec(std::initializer_list<int>(__a, __a + 2));

However, this has to be read in context - for example, the array backing the initializer_list has lifetime bounded by the initialization of the vector.

This was a lot clearer in C++03, which had in [intro.execution]:

13 - [Note: certain contexts in C++ cause the evaluation of a full-expression that results from a syntactic construct other than expression (5.18). For example, in 8.5 one syntax for initializer is ( expression-list ) but the resulting construct is a function call upon a constructor function with expression-list as an argument list; such a function call is a full-expression. For example, in 8.5, another syntax for initializer is = initializer-clause but again the resulting construct might be a function call upon a constructor function with one assignment-expression as an argument; again, the function call is a full-expression. ]

This paragraph is struck in its entirety from C++11; this was per the resolution to CWG 392. The resulting confusion was presumably not intended.