GCC 的实现std::initializer_list
在返回完整表达式的末尾销毁从函数返回的数组。它是否正确?
该程序中的两个测试用例都显示了在值可以使用之前执行的析构函数:
#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 );
}
我认为该程序应该可以工作。但是底层的标准语有点复杂。
return 语句初始化一个返回值对象,就像它被声明一样
std::initializer_list< noisydt > ret = { {},{},{} };
这从给定的一系列初始化程序初始化一个临时initializer_list
及其底层数组存储,然后initializer_list
从第一个初始化另一个。阵列的寿命是多少?“数组的生命周期与对象的生命周期相同initializer_list
。” 但是其中有两个;哪一个是模棱两可的。8.5.4/6 中的示例,如果它像宣传的那样工作,应该解决数组具有复制到对象的生命周期的歧义。然后返回值的数组也应该存在于调用函数中,并且应该可以通过将其绑定到命名引用来保存它。
在LWS上,GCC 在返回之前错误地杀死了数组,但它initializer_list
根据示例保留了一个命名。Clang 也正确处理了示例,但列表中的对象永远不会被破坏;这会导致内存泄漏。ICC根本不支持initializer_list
。
我的分析正确吗?
C++11 §6.6.3/2:
带有花括号初始化列表的 return 语句通过指定初始化列表中的复制列表初始化 (8.5.4) 初始化要从函数返回的对象或引用。
8.5.4/1:
…复制初始化上下文中的列表初始化称为复制列表初始化。
8.5/14:
以 ...形式发生的初始化
T x = a;
称为复制初始化。
回到 8.5.4/3:
T 类型的对象或引用的列表初始化定义如下:……</p>
— 否则,如果 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
。[例子: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
andv2
,创建的initializer_list
对象和数组{ 1, 2, 3 }
具有完整的表达式生命周期。对于i3
,initializer_list 对象和数组具有自动生命周期。——结束示例]
关于返回一个花括号初始化列表的一点说明
当您返回用大括号括起来的裸列表时,
带有花括号初始化列表的 return 语句通过指定初始化列表中的复制列表初始化 (8.5.4) 初始化要从函数返回的对象或引用。
这并不意味着返回到调用范围的对象是从某物复制而来的。例如,这是有效的:
struct nocopy {
nocopy( int );
nocopy( nocopy const & ) = delete;
nocopy( nocopy && ) = delete;
};
nocopy f() {
return { 3 };
}
这不是:
nocopy f() {
return nocopy{ 3 };
}
Copy-list-initialization 仅表示使用等效的语法nocopy X = { 3 }
来初始化表示返回值的对象。这不会调用副本,它恰好与 8.5.4/6 中延长数组生命周期的示例相同。
Clang 和 GCC 确实同意这一点。
其他注意事项
对N2640的评论并没有提到这个角落案例。已经对这里组合的各个功能进行了广泛的讨论,但我没有看到任何关于它们的交互的信息。
实现这一点很麻烦,因为它归结为按值返回一个可选的可变长度数组。因为std::initializer_list
它不拥有它的内容,所以该函数还必须返回其他拥有它的东西。当传递给函数时,这只是一个本地的、固定大小的数组。std::initializer_list
但在另一个方向上,VLA 需要与' 的指针一起返回到堆栈中。然后需要告诉调用者是否处理序列(无论它们是否在堆栈上)。
这个问题很容易通过从 lambda 函数返回一个花括号初始化列表来偶然发现,作为一种“自然”的方式来返回一些临时对象而不关心它们是如何包含的。
auto && il = []() -> std::initializer_list< noisydt >
{ return { noisydt{}, noisydt{} }; }();
确实,这与我到达这里的方式相似。但是,省略 trailing-return-type 将是错误的,->
因为 lambda 返回类型推导仅在返回表达式时发生,并且花括号初始化列表不是表达式。