43

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

ead
  • 32,758
  • 6
  • 90
  • 153
  • 3
    `arr` is using aggregate initialization, and `vec` is using a constructor call. – Kerrek SB Aug 18 '16 at 18:44
  • 2
    Seems a bug with gcc, clang gives `1, 2` for both [Demo](http://coliru.stacked-crooked.com/a/d4a3cc71535bb874) – Jarod42 Aug 18 '16 at 18:46
  • The full expression rather is "ID().id" here, afair. However in the vector case, the most outer expression is the constructor call to the initializer list constructor. What happens for `auto &&arr2 = decltype(arr){ID().id, ID().id};`? – Johannes Schaub - litb Aug 18 '16 at 18:48
  • FWIW MSVS 2015 update 3 gives the same results as g++. – NathanOliver Aug 18 '16 at 19:06
  • @JohannesSchaub-litb `auto &&arr2 = decltype(arr){ID().id, ID().id};` results in `{1,1}` if compiled with g++ – ead Aug 18 '16 at 19:21

1 Answers1

19

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.

ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • @NathanOliver yup. – ecatmur Aug 18 '16 at 19:06
  • 4
    Just tested MSVS and it behaves like g++. It is a little concerning that all three major vendors got this wrong. – NathanOliver Aug 18 '16 at 19:07
  • 1
    My DR was only about cases like "int a = 5; int b = a++". There is a corresponding SO question I made at that time. However I did not make a DR about braced init lists and I don't.have an opinion on what are the intended rules regarding the example in this question. At least not without having dine a survey throughout the 90's drafts and the most recent developments :) – Johannes Schaub - litb Aug 18 '16 at 20:37
  • There also seems to be a yet unanswered question on this matter aswell http://stackoverflow.com/questions/6315670/when-an-array-is-created-by-a-subexpression-what-happens-with-the-temporaries-t – Johannes Schaub - litb Aug 18 '16 at 20:45
  • 1
    Here is the question that the DR was based on http://stackoverflow.com/questions/5760866/when-exactly-is-an-initializer-temporary-destroyed – Johannes Schaub - litb Aug 18 '16 at 20:49
  • Thanks for your great explanations! If I got it right, you are implying, that both initiliazations should yield the same result. I find it hard to believe, that g++, msvs and intel - all got it wrong (see my update). Also clang does not follow the standard as it is. I'm a little bit at loss here... – ead Aug 19 '16 at 07:10
  • @ead I seem to remember that there was recent talk sonewhere about this by the clang smith, so perhaps clangs behavior is on purpose? – Johannes Schaub - litb Aug 19 '16 at 09:00
  • Can you please show cases where the term function is not intended to also apply to constructors? That would be new to me – Johannes Schaub - litb Aug 19 '16 at 17:28
  • @JohannesSchaub-litb [expr.const] for one. – ecatmur Aug 20 '16 at 19:33
  • 1
    @ecatmur do you refer to the terms "constexpr function" and "constexpr constructor"?. A "constexpr function" is not a function that is constexpr, but "constexpr function" is a defined non-compound term: "A constexpr specifier used in the declaration of a function that is not a constructor declares that function to be a *constexpr function*." SO when expr.const says "constexpr function" it does not say "function that is constexpr", but actually refers to that definition of "constexpr function". – Johannes Schaub - litb Aug 20 '16 at 20:18
  • 1
    Note that expr.const itself has as second bullet "an invocation of a function other than a constexpr constructor", which exemplifies that constructors are treated to be functions in that paragraph. – Johannes Schaub - litb Aug 20 '16 at 20:20
  • @JohannesSchaub-litb OK, I'm convinced, thanks for taking the time to explain. I'll revise my answer accordingly. – ecatmur Aug 22 '16 at 09:49
  • @ecatmur Where exactly in the standard does it say that "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"? I found this: http://eel.is/c++draft/dcl.init#list-4, but it doesn't explicitly use "full-expression". – user42768 Aug 07 '17 at 22:07
  • @ecatmur Is it because they are expressions and they are not part of any other expression, nor any other full-expression? And when the construct containing the braced-list implicitly calls a function (in the case of the vector constructor), becoming a full-expression, now the expressions separated by commas are part of a full-expression? – user42768 Aug 08 '17 at 08:38
  • @user42768 yes, exactly. It looks like this may have been changed recently http://eel.is/c++draft/intro.execution#12 so that an init-declarator is a full-expression - I'll research the change and update my answer. – ecatmur Aug 08 '17 at 08:46
  • @ecatmur This question lead me to ask this: https://stackoverflow.com/questions/45576309/c-17-full-expression-standard-definition. The first point was answered in the comments, and for the last two points I asked here: https://groups.google.com/a/isocpp.org/forum/#!topic/std-discussion/tBsNNupi0zU. Apparently, there is some bad wording in the standard, and an init-declarator is not a full-expression, but the initialization done by it is (second link). However, I found another contradiction described in the second link as well (in my response). Can you please tell me if I'm right? – user42768 Aug 11 '17 at 08:39
  • 1
    @user42768 If the Standard says that an init-declarator is a full-expression, then that's what it is; the comment on the example is incorrect or misleading. The confusion may arise from the fact that in that case the only action the full-initializer performs is the call to `S::S(int)`. An init-declarator is not a construct defined to produce an implicit call to a function; an example of such would be an `if` statement, which is defined to implicitly convert the result of its `condition` (an expression) to `bool`. – ecatmur Aug 11 '17 at 09:50
  • @ecatmur Thank you for the response. So, finally, the conclusion is that the comment is wrong. However, http://eel.is/c++draft/class.base.init#7.note-1 says that: "The **initialization** performed by each mem-initializer constitutes a full-expression [...].". I tried to track the changes made to the paragraph defining full-expressions and I found this: http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#392. The removed text suggests (at least to me) that an init-declarator produces an implicit function call. – user42768 Aug 11 '17 at 10:22
  • 1
    @user42768 Again, a note is non-normative; the text of the standard takes precedence. An init-declarator produces an implicit call to a function only if the class has a user-defined constructor, in which case the call to the constructor function is considered to be an expression that is part of the full-expression that is the init-declarator. If the class is an aggregate there will be no implicit function call, but the init-declarator is still a full-expression. – ecatmur Aug 11 '17 at 13:48
  • Now everything seems to be clear. To conclude, that comment in the standard is mistaken then. Thank you very much for taking the time to explain. – user42768 Aug 11 '17 at 14:30