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.