列表初始化期间临时对象的生存期
我一直认为,临时性的东西一直存在,直到一个完整的expression式结束。 然而这是一个std::vector
和一个数组的初始化之间的一个奇怪的区别。
请考虑下面的代码:
#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"; }
这个程序的输出是有点意外的(至less对我来说):
Array: 1, 1 Vector: 1, 2
这意味着,临时对象在std::vector
的整个初始化期间是活着的,但是在数组的情况下它们是相继创build和析构的。 我期望的临时生活,直到完整expression式为止int arr[]{ID().id, ID().id};
完成了。
该标准提到了关于临时对象的生命周期和数组初始化的一个例外(12.2)。 但是我没有明白它的含义,也不知道为什么它适用于这个特殊情况:
有两种情况下临时销毁在不同于完整expression式结束的点上。 第一个上下文是当一个默认构造函数被调用来初始化一个数组的元素时。 如果构造函数有一个或多个默认参数,则在构造下一个数组元素(如果有的话)之前,对在默认参数中创build的每个临时对象的销毁进行sorting。
使用不同编译器的结果概述(MSVS结果是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
ecatmur指出,对于聚合初始化,braced-init-list的每个元素都是一个完整expression式,因此下面的代码
struct S{ int a; int b; } s{ID().id, ID().id}; std::cout<<" Struct: "<<sa<<", "<<sb<<"\n";
应该将Struct 1, 1
打印到控制台。 这正是g ++编译的程序所做的。 然而,铿锵似乎有一个错误 – 结果程序打印Struct 1, 2
。
已经有一个bug报告: https ://llvm.org/bugs/show_bug.cgi ? id = 29080
这是核心问题1343“非类别初始化的sorting” ,于2016年11月被论文P0570R0接受为缺陷报告。 所提出的解决scheme是C ++ 17的一部分,但不是C ++ 14的一部分,因此(除非委员会决定发布C ++ 14的更正),这是C ++ 17和C + +14。
C ++ 14
根据C ++ 14标准的规则,正确的输出是1, 1
数组1, 2
和1, 2
向量1, 2
; 这是因为构造一个向量(包括从一个braced-init-list )需要调用构造函数,而构造一个数组则不需要。
控制这个的语言在[intro.execution]中 :
10 – 全expression式是不是另一个expression式的子expression式的expression式。 […]如果一个语言结构被定义为产生一个函数的隐式调用,那么这个语言结构的使用被认为是为了这个定义的expression。 […]
作为顶级的概述,这没什么问题,但是它还是没有解答一些问题:
- 确切地说,哪一种语言结构被视为产生函数的隐式调用的构造;
- 什么实际上被视为一个函数的隐式调用; 大概是一个用户定义的构造函数的调用是一个函数的调用,但是默认或定义为默认的构造函数呢?
一个数组是一个聚集,所以根据[dcl.init.aggr]从一个braced -init-list初始化 ; 这就是说每个元素都是直接从列表的相应元素初始化的,所以没有隐式的函数调用(至less不对应于整体的初始化)。 在语法级别上,使用braced -init-list作为括号或等于初始值设定项的初始化程序 ( [dcl.init] / 1)中,完整expression式是包含在大括号内的expression式,并用逗号分隔。 在每个完整expression式的结尾处,临时对象的析构函数被要求运行,因为在这里[class.temporary]中提到的三个上下文都没有。
向量初始化的情况是不同的,因为你正在使用initializer_list
构造函数,所以发生了函数的隐式调用(即initializer_list
构造函数); 这意味着在整个初始化过程中有一个隐含的完整expression式,所以只有在vector的初始化完成时才会销毁临时对象。
令人困惑的是, [dcl.init.list]说你的代码“大致相当于”:
const int __a[2] = {int{ID().id}, int{ID().id}}; // #1 std::vector<int> vec(std::initializer_list<int>(__a, __a + 2));
但是,这必须在上下文中进行读取,例如,支持initializer_list
的数组的生命周期由向量的初始化界定。
这在C ++ 03中已经很清楚了,它在[intro.execution]中有 :
13 – [ 注意: C ++中的某些上下文导致评估由expression式 (5.18)以外的语法结构产生的完整expression式 。 例如,在8.5中, 初始化程序的一个语法是
( expression-list )
但是结果的结构是一个函数调用,其构造函数的expression式列表作为参数列表; 这样的函数调用是一个完整的expression式。 例如,在8.5中, 初始化程序的另一个语法是= initializer-clause
但是结果构造函数也可能是一个构造函数的函数调用,其中一个赋值expression式作为参数; 再次,函数调用是一个完整的expression式。 ]
本段落全部来自C ++ 11; 这是根据CWG 392的决议。 由此产生的混乱大概是没有打算的。
C ++ 17
在P0570R0之后, [intro.execution]指出一个完整的expression式是:[…]
- 初始化声明符([dcl.decl]),包括初始化符的构成expression式,
- 一个expression式不是另一个expression式的子expression式,而不是其他expression式的一部分。
所以在C ++ 17中,完整expression式分别是arr[]{ID().id, ID().id}
和vec{ID().id, ID().id}
,正确的输出是1, 2
在每种情况下,由于第一个临时ID
的销毁被推迟到全面expression的结尾。