Pot*_*ter 25 c++ lifetime initializer-list c++11 list-initialization
GCC的实现破坏了std::initializer_list
返回full-expression结束时从函数返回的数组.它是否正确?
此程序中的两个测试用例都显示在使用值之前执行的析构函数:
#include <initializer_list>
#include <iostream>
struct noisydt {
~noisydt() { std::cout << "destroyed\n"; }
};
void receive( std::initializer_list< noisydt > il ) {
std::cout << "received\n";
}
std::initializer_list< noisydt > send() {
return { {}, {}, {} };
}
int main() {
receive( send() );
std::initializer_list< noisydt > && il = send();
receive( il );
}
Run Code Online (Sandbox Code Playgroud)
我认为该计划应该有效.但潜在的标准有点令人费解.
return语句初始化一个返回值对象,就像它被声明一样
std::initializer_list< noisydt > ret = { {},{},{} };
Run Code Online (Sandbox Code Playgroud)
这initializer_list
将从给定的初始化器系列初始化一个临时及其底层数组存储,然后initializer_list
从第一个初始化器初始化另一个.阵列的寿命是多少?"数组的生命周期与initializer_list
对象的生命周期相同." 但其中有两个; 哪一个是模棱两可的.8.5.4/6中的示例(如果它按照公布的方式工作)应该解决数组具有复制到对象的生命周期的歧义.然后返回值的数组也应该存在于调用函数中,并且应该可以通过将它绑定到命名引用来保留它.
在LWS上,GCC在返回之前错误地杀死了数组,但它initializer_list
根据示例保留了一个命名.Clang也正确处理了这个例子,但是列表中的对象永远不会被销毁; 这会导致内存泄漏.国际刑事法院根本不支持initializer_list
.
我的分析是否正确?
C++11§6.6.3/ 2:
带有braced-init-list的return语句通过copy-list-initialization(8.5.4)从指定的初始化列表初始化要从函数返回的对象或引用.
8.5.4/1:
...复制初始化上下文中的列表初始化称为复制列表初始化.
8.5/14:
表单中发生的初始化
T x = a;
称为复制初始化.
回到8.5.4/3:
列表初始化对象或类型T的引用定义如下:...
- 否则,如果T是特化
std::initializer_list<E>
,initializer_list
则如下所述构造对象,并根据相同类型的类(8.5)的对象初始化规则初始化对象.
8.5.4/5:
类型的对象
std::initializer_list<E>
是从初始化列表构造的,就好像实现分配了类型为E的N个元素的数组,其中N是初始化列表中的元素数.使用初始化列表的相应元素对该数组的每个元素进行复制初始化,并构造该对象以引用该数组.如果需要缩小转换来初始化任何元素,则程序格式不正确.std::initializer_list<E>
8.5.4/6:
数组的生命周期与
initializer_list
对象的生命周期相同.[例:Run Code Online (Sandbox Code Playgroud)typedef std::complex<double> cmplx; std::vector<cmplx> v1 = { 1, 2, 3 }; void f() { std::vector<cmplx> v2{ 1, 2, 3 }; std::initializer_list<int> i3 = { 1, 2, 3 }; }
对于
v1
和v2
,创建的initializer_list
对象和数组{ 1, 2, 3 }
具有完整表达式生命周期.因为i3
,initializer_list对象和数组具有自动生命周期. - 结束例子]
当您返回括在括号中的裸列表时,
带有braced-init-list的return语句通过copy-list-initialization(8.5.4)从指定的初始化列表初始化要从函数返回的对象或引用.
这并不意味着返回到调用范围的对象是从某些东西复制的.例如,这是有效的:
struct nocopy {
nocopy( int );
nocopy( nocopy const & ) = delete;
nocopy( nocopy && ) = delete;
};
nocopy f() {
return { 3 };
}
Run Code Online (Sandbox Code Playgroud)
这不是:
nocopy f() {
return nocopy{ 3 };
}
Run Code Online (Sandbox Code Playgroud)
复制列表初始化只是意味着语法的等价物nocopy X = { 3 }
用于初始化表示返回值的对象.这不会调用副本,它恰好与数组生命周期扩展的8.5.4/6示例相同.
Clang和GCC 就这一点达成一致.
对N2640的评论没有提到这个角落的情况.关于这里结合的各个特征已经有了广泛的讨论,但我没有看到他们的互动.
实现它会变得毛茸茸,因为它返回按值返回可选的可变长度数组.因为std::initializer_list
它不拥有其内容,所以该函数还必须返回其他内容.传递给函数时,这只是一个固定大小的本地数组.但是在另一个方向上,VLA需要与std::initializer_list
指针一起返回堆栈.然后需要告知调用者是否处理序列(无论它们是否在堆栈中).
通过从lambda函数返回一个braced-init-list,这个问题很容易发生,作为返回一些临时对象而不关心它们如何被包含的"自然"方式.
auto && il = []() -> std::initializer_list< noisydt >
{ return { noisydt{}, noisydt{} }; }();
Run Code Online (Sandbox Code Playgroud)
实际上,这与我到达这里的方式类似.但是,省略->
trailing-return-type 将是一个错误,因为lambda返回类型推导仅在返回表达式时发生,并且braced-init-list不是表达式.
Jon*_*ely 18
std::initializer_list
不是一个容器,不要用它来传递值并期望它们持久存在
DR 1290更改了措辞,您还应该知道尚未准备好的1565和1599.
然后返回值的数组也应该活到调用函数,它应该能够通过其绑定到一个名为参考保存它.
不,这不遵循.数组的生命周期不会随着数据的延长而延长initializer_list
.考虑:
struct A {
const int& ref;
A(const int& i = 0) : ref(i) { }
};
Run Code Online (Sandbox Code Playgroud)
引用i
绑定到临时int
,然后引用也ref
绑定到它,但是这不延长生命周期i
,它仍然超出构造函数末尾的范围,留下悬空引用.您不会通过绑定对它的另一个引用来延长底层临时生命周期.
如果1565获得批准并且您复制而不是参考,那么您的代码可能会更安全,但该问题仍然存在,甚至没有提出措辞,更不用说实施经验了.il
即使你的例子是有用的,关于底层数组生命周期的措辞显然仍在改进,编译器需要一段时间来实现最终的语义.
Ric*_*ith 13
您在8.5.4/6中提到的措辞有缺陷,并在某种程度上由DR1290进行了修正.而不是说:
数组的生命周期与
initializer_list
对象的生命周期相同.
...修改后的标准现在说:
该数组与任何其他临时对象(12.2 [class.temporary])具有相同的生命周期,除了
initializer_list
从数组初始化对象延长了数组的生命周期,就像绑定对临时对象的引用一样.
因此,临时阵列的生命周期控制措辞为12.2/5,其中说:
函数返回语句中返回值临时绑定的生命周期未扩展; 临时在return语句中的full-expression结束时被销毁
因此,noisydt
在函数返回之前销毁对象.
直到最近,Clang还有一个错误导致它initializer_list
在某些情况下无法破坏对象的底层数组.我已经解决了Clang 3.4; 来自Clang主干的测试用例的输出是:
destroyed
destroyed
destroyed
received
destroyed
destroyed
destroyed
received
Run Code Online (Sandbox Code Playgroud)
......根据DR1290,这是正确的.