Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lifetime of temporary objects during list-initialization

Tags:

c++

c++11

g++

clang

I always assumed, that temporary objects live until the end of a full-expression. Here is however a curious difference between initializations of a std::vector 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;    ID():id(++cnt){}    ~ID(){      cnt--;   } };  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 std::vector 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.


A bug has been reported to clang: https://llvm.org/bugs/show_bug.cgi?id=29080

like image 213
ead Avatar asked Aug 18 '16 18:08

ead


People also ask

What is the lifetime of an object in c++?

C/C++ use lexical scoping. The lifetime of a variable or object is the time period in which the variable/object has valid memory. Lifetime is also called "allocation method" or "storage duration."

What is the lifetime of an object and how can you extend the lifetime of an object?

The lifetime of a temporary object may be extended by binding to a const lvalue reference or to an rvalue reference (since C++11), see reference initialization for details.

What is lifetime of an object?

In object-oriented programming (OOP), the object lifetime (or life cycle) of an object is the time between an object's creation and its destruction.

When compiler create temporary object in c++?

True temporary objects in C++ are invisible - they don't appear in your source code. They arise whenever a non-heap object is created but not named. Such unnamed objects usually arise in one of two situations: when implicit type conversions are applied to make function calls succeed and when functions return objects.


1 Answers

This is core issue 1343 "Sequencing of non-class initialization", which was accepted as a Defect Report in November 2016 by paper P0570R0. The resolution proposed is part of C++17 but not therefore part of C++14, so (unless the committee decide to publish a corrigendum to C++14) this is a point of difference between C++17 and C++14.

C++14

The correct output according to the rules of the C++14 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.

C++17

After P0570R0, [intro.execution] states that a full-expression is: [...]

  • an init-declarator ([dcl.decl]) [...] including the constituent expressions of the initializer, or [...]
  • an expression that is not a subexpression of another expression and that is not otherwise part of a full-expression.

So in C++17, the full-expression is arr[]{ID().id, ID().id} and vec{ID().id, ID().id} respectively, and the correct output is 1, 2 in each case, since the destruction of the first temporary ID is deferred to the end of the full-expression.

like image 170
ecatmur Avatar answered Sep 20 '22 15:09

ecatmur