c++ - std::initializer_list 返回值的生命周期

标签 c++ c++11 initializer-list lifetime list-initialization

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 中的示例,如果它像宣传的那样工作,应该解决数组具有复制到对象的生命周期的歧义。然后返回值的数组也应该存在于调用函数中,并且应该可以通过将其绑定(bind)到命名引用来保存它。

开启 LWS , GCC 在返回之前错误地杀死了数组,但它保留了一个名为 initializer_list根据示例。 Clang 也可以正确处理示例,但列表中的对象从不 销毁;这会导致内存泄漏。 ICC 不支持 initializer_list完全没有。

我的分析正确吗?


C++11 §6.6.3/2:

A return statement with a braced-init-list initializes the object or reference to be returned from the function by copy-list-initialization (8.5.4) from the specified initializer list.

8.5.4/1:

… list-initialization in a copy-initialization context is called copy-list-initialization.

8.5/14:

The initialization that occurs in the form T x = a; … is called copy-initialization.

回到 8.5.4/3:

List-initialization of an object or reference of type T is defined as follows: …

— Otherwise, if T is a specialization of std::initializer_list<E>, an initializer_list object is constructed as described below and used to initialize the object according to the rules for initialization of an object from a class of the same type (8.5).

8.5.4/5:

An object of type std::initializer_list<E> is constructed from an initializer list as if the implementation allocated an array of N elements of type E, where N is the number of elements in the initializer list. Each element of that array is copy-initialized with the corresponding element of the initializer list, and the std::initializer_list<E> object is constructed to refer to that array. If a narrowing conversion is required to initialize any of the elements, the program is ill-formed.

8.5.4/6:

The lifetime of the array is the same as that of the initializer_list object. [Example:

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

For v1 and v2, the initializer_list object and array createdfor { 1, 2, 3 } have full-expression lifetime. For i3, the initializer_list object and array have automatic lifetime. — end example]


关于返回花括号初始化列表的一点说明

当你返回一个用大括号括起来的裸列表时,

A return statement with a braced-init-list initializes the object or reference to be returned from the function by copy-list-initialization (8.5.4) from the specified initializer list.

这并不意味着返回到调用范围的对象是从某物复制而来的。例如,这是有效的:

struct nocopy {
    nocopy( int );
    nocopy( nocopy const & ) = delete;
    nocopy( nocopy && ) = delete;
};

nocopy f() {
    return { 3 };
}

这不是:

nocopy f() {
    return nocopy{ 3 };
}

复制列表初始化仅仅意味着语法nocopy X = { 3 }的等价物。用于初始化表示返回值的对象。这不会调用拷贝,它恰好与 8.5.4/6 中延长数组生命周期的示例相同。

Clang 和 GCC 会做 agree关于这一点。


其他说明

N2640 的评论没有提到这个极端案例。已经对此处组合的各个功能进行了广泛的讨论,但我看不到它们之间的交互。

实现这一点很麻烦,因为它归结为按值返回一个可选的可变长度数组。因为 std::initializer_list不拥有其内容,该函数还必须返回其他内容。当传递给函数时,这只是一个本地的、固定大小的数组。但在另一个方向,VLA 需要与 std::initializer_list 一起返回到堆栈中。的指针。然后需要告知调用者是否处理序列(无论它们是否在堆栈上)。

这个问题很容易通过从 lambda 函数返回一个花括号初始化列表来偶然发现,这是一种“自然”的方式来返回一些临时对象而不关心它们是如何包含的。

auto && il = []() -> std::initializer_list< noisydt >
               { return { noisydt{}, noisydt{} }; }();

确实,这与我到达这里的方式相似。但是,省略 -> 将是错误的。 trailing-return-type,因为 lambda 返回类型推导只发生在返回表达式时,而花括号初始化列表不是表达式。

最佳答案

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

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

Then the return value's array should also survive into the calling function, and it should be possible to preserve it by binding it to a named reference.

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

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

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

如果 1565,您的代码可能会更安全被批准并且您将 il 制作为拷贝而不是引用,但该问题仍然存在,甚至没有建议的措辞,更不用说实现经验了。

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

关于c++ - std::initializer_list 返回值的生命周期,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15286450/

相关文章:

c++ - 为什么在传递 lambda 而不是函数指针时无法推断模板参数

c++ - 将条目添加到双端队列 C++

c# - C++ vector 与C#列表。他们为什么产生不同的结果?

c++ - 从另一个线程发出信号的最快方法

c++ - 数组初始化结构

c++ - 此输出有效还是编译器错误?

c++ - C++ 计算字符串中辅音的逻辑错误

c++ - 用于 xml 标签内容的单一匹配的正则表达式

c++ - 可变参数模板和复制构造函数

c++ - c++11 中 std::initializer_list 的好处