std :: initializer_list返回值的生命周期

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>是从初始化列表构造的,就好像实现分配了类型为EN个元素的数组,其中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 };
 }
Run Code Online (Sandbox Code Playgroud)

对于v1v2,创建的initializer_list对象和数组{ 1, 2, 3 }具有完整表达式生命周期.因为i3,initializer_list对象和数组具有自动生命周期. - 结束例子]


关于返回braced-init-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更改了措辞,您还应该知道尚未准备好的15651599.

然后返回值的数组也应该活到调用函数,它应该能够通过其绑定到一个名为参考保存它.

不,这不遵循.数组的生命周期不会随着数据的延长而延长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,这是正确的.

  • 更新:大约上周(2018 年 7 月下旬),Clang trunk 现在发出警告,并显示“本地临时对象的返回地址”错误。 (2认同)