该程序中的两个测试用例都显示析构函数在值被使用之前执行:
#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。那么数组的生命周期是什么?“数组的生命周期与initializer_list对象相同。”但有两个这样的对象;哪一个是模糊的。如果8.5.4/6中的示例按照广告运作,则应解决数组具有复制到对象的生命周期的歧义。然后返回值的数组也应该在调用函数中保持存在,并且可以通过将其绑定到命名引用来保留它。
在LWS上,GCC错误地在返回之前杀死了数组,但根据示例保留了一个命名的initializer_list。Clang也正确处理示例,但列表中的对象永远不会被销毁;这会导致内存泄漏。ICC根本不支持initializer_list。
我的分析正确吗?
C++11 §6.6.3/2:
使用带括号的初始化列表的返回语句,通过从指定的初始化列表进行复制列表初始化(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:
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]
关于返回花括号初始化列表的一点澄清
当你返回一个被括在花括号中的裸列表时,
带有花括号初始化列表的返回语句通过从指定的初始化列表进行复制列表初始化(8.5.4)来初始化将要从函数返回的对象或引用。
这并不意味着返回到调用作用域的对象是从某个地方复制的。例如,以下代码是有效的:
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{} }; }();
事实上,这与我到达此处的方式类似。但是,省略 ->
尾随返回类型是错误的,因为 lambda 返回类型推断仅在返回表达式时发生,而大括号初始化列表不是表达式。
noisydt
的复制构造。请注意,复制初始化列表不会复制底层数组。 - Potatoswatterstd::string
。 - Potatoswatterreturn std::initializer_list<noisydt>{ … };
则是另一回事,因为然后列表初始化将应用于一个附加的临时对象,其生命周期将决定数组的生命周期。 - Potatoswatter