33

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 };
 }

对于v1and v2,创建的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 返回类型推导仅在返回表达式时发生,并且花括号初始化列表不是表达式。

4

2 回答 2

23

std::initializer_list不是容器,不要用它来传递值并期望它们持续存在

DR 1290更改了措辞,您还应该注意尚未准备好的15651599 。

然后返回值的数组也应该存在于调用函数中,并且应该可以通过将其绑定到命名引用来保存它。

不,这不符合。数组的生命周期不会随着initializer_list. 考虑:

struct A {
    const int& ref;
    A(const int& i = 0) : ref(i) { }
};

引用i绑定到临时的int,然后引用ref也绑定到它,但这并没有延长 的生命周期i,它仍然在构造函数的末尾超出范围,留下一个悬空引用。您不会通过绑定另一个对它的引用来延长底层临时的生命周期。

如果1565获得批准并且您制作副本而不是参考,您的代码可能会更安全,但该问题仍然存在,甚至没有提议的措辞,更不用说实施经验了。il

即使您的示例旨在工作,关于底层数组生命周期的措辞显然仍在改进,编译器需要一段时间才能实现最终确定的语义。

于 2013-03-08T09:54:37.203 回答
19

您在 8.5.4/6 中提到的措辞是有缺陷的,并已由DR1290 (在某种程度上)纠正。而不是说:

数组的生命周期与对象的生命周期相同initializer_list

... 修订后的标准现在说:

该数组与任何其他临时对象(12.2 [class.temporary])具有相同的生命周期,除了initializer_list从数组初始化对象扩展了数组的生命周期,就像将引用绑定到临时对象一样。

因此,临时数组生命周期的控制措辞是 12.2/5,它表示:

临时绑定到函数返回语句中的返回值的生命周期不会延长;临时在 return 语句中的完整表达式的末尾被销毁

因此,noisydt对象在函数返回之前被销毁。

initializer_list直到最近,Clang 还存在一个错误,导致它在某些情况下无法销毁对象的底层数组。我已经为 Clang 3.4 修复了这个问题;Clang 中继的测试用例的输出是:

destroyed
destroyed
destroyed
received
destroyed
destroyed
destroyed
received

...这是正确的,根据 DR1290。

于 2013-07-14T00:18:33.907 回答