std :: initializer_list返回值的生命周期 [英] lifetime of a std::initializer_list return value
问题描述
GCC的实现破坏了从返回full-expression末尾的函数返回的 std :: initializer_list
数组。这是正确的吗?
这个程序中的两个测试用例都显示在可以使用值之前执行的析构函数:
#include< initializer_list>
#include< iostream>
struct noisydt {
〜noisydt(){std :: cout< destroyed\\\
; }
};
void receive(std :: initializer_list< noisydt> il){
std :: cout< received\\\
;
}
std :: initializer_list< noisydt> send(){
return {{},{},{}};
}
int main(){
receive(send());
std :: initializer_list< noisydt> &&& il = send();
receive(il);
}
我认为程序应该工作。
返回语句初始化一个返回值对象,就像它被声明
std :: initializer_list< noisydt> ret = {{},{},{}};
这会初始化一个临时 initializer_list
数组存储从给定的一系列初始化器,然后初始化另一个 initializer_list
从第一个。什么是数组的生命周期? 数组的生命周期与 initializer_list
对象的生命周期相同。但有两个;哪一个是模糊的。在8.5.4 / 6中的示例,如果它如所通告的那样工作,应当解决数组具有复制到对象的生存期的模糊性。
http://liveworkspace.org/code/2ZlWsj%240\"> LWS ,GCC在返回之前错误地删除了数组,但它保留了一个名为
initializer_list
的例。 Clang还会正确处理该示例,但列表中的对象永远不会被销毁;这将导致内存泄漏。 ICC不支持 initializer_list
。 我的分析结果是否正确?
C ++ 11§6.6.3/ 2:
具有 braced-init-list 的返回语句通过指定的初始化器列表中的copy-list-initialization(8.5.4)初始化要从函数返回的对象或引用。
8.5.4 / 1:
... list-在复制初始化上下文中的初始化称为
8.5 / 14:/ p>
以
返回到8.5.4 / 3:T x = a;
...的形式发生的初始化称为< 。
$ b b $ b
类型T的对象或引用的列表初始化定义如下:...
否则,如果T是
std :: initializer_list< E>
的特殊化,initializer_list
对象如下所述被构造并用于初始化
8.5.4 / 5(对象的初始化规则) :
std :: initializer_list< E>
初始化器列表,好像实现分配了类型为 E 的 N 元素的数组,其中 N 是初始化器列表中的元素数目。该数组的每个元素都用初始化器列表的相应元素进行复制初始化,并且构造std :: initializer_list
对象来指代该数组。
8.5.4 / 6:
如果需要缩小转换以初始化任何元素, / p>
数组的生命周期与
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
v2
,为{1,2,3}创建的
有全表达式生命周期。对于initializer_list
对象和数组i3
,initializer_list对象和数组具有自动生命周期。 - end example]
a braced-init-list
当您返回括号括起来的裸列表时,
带有braced-init-list的返回语句通过指定的初始化器列表中的copy-list-initialization(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做< a href =http://liveworkspace.org/code/ClVhK%241>同意这一点。
其他注意事项
N2640 不会提及这种情况。关于这里的个别功能已经进行了广泛的讨论,但我没有看到任何关于他们的互动。
实现这是毛茸茸的,因为它返回一个可选的,可变长度数组的值。因为
std :: initializer_list
不拥有它的内容,该函数还必须返回其他的东西。当传递给一个函数时,这只是一个局部的,固定大小的数组。但在另一个方向,VLA需要在栈上返回,以及std :: initializer_list
的指针。然后调用者需要被告知是否处理序列(不管他们是否在栈上)。
这个问题很容易绊倒一个来自lambda函数的braced-init-list,作为一种自然的方式返回一些临时对象而不关心它们如何包含。
code>自动&& il = []() - > std :: initializer_list< noisydt>
{return {noisydt {},noisydt {}}; }();
确实,这是类似于我到达这里。但是,省略
- >
trailing-return-type将是一个错误,因为lambda返回类型的扣除只发生在返回一个表达式时,而braced-init -list不是表达式。解决方案您在8.5.4 / 6中提到的措辞有缺陷,有些), DR1290 。而不是说:
数组的生命周期与
initializer_list
object。
...修改后的标准现在说:
< >
该数组与任何其他临时对象(12.2 [class.temporary])具有相同的生命周期,除了从数组初始化一个
initializer_list
因此,临时数组生命周期的控制措辞是12.2 / 5,它表示:
函数return语句中返回值的临时绑定的生命周期不会扩展;
因此, noisydt code>对象在函数返回之前被销毁。
直到最近,Clang还有一个错误导致它无法破坏 initializer_list
对象。我已经修复了Clang 3.4;您的测试用例从Clang trunk的输出是:
销毁
销毁
销毁
收到
销毁
销毁
销毁
收到
...这是正确的,根据DR1290。
GCC's implementation destroys a std::initializer_list
array returned from a function at the end of the return full-expression. Is this correct?
Both test cases in this program show the destructors executing before the value can be used:
#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 );
}
I think the program should work. But the underlying standardese is a bit convoluted.
The return statement initializes a return value object as if it were declared
std::initializer_list< noisydt > ret = { {},{},{} };
This initializes one temporary initializer_list
and its underlying array storage from the given series of initializers, then initializes another initializer_list
from the first one. What is the array's lifetime? "The lifetime of the array is the same as that of the initializer_list
object." But there are two of those; which one is ambiguous. The example in 8.5.4/6, if it works as advertised, should resolve the ambiguity that the array has the lifetime of the copied-to object. 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.
On LWS, GCC erroneously kills the array before returning, but it preserves a named initializer_list
per the example. Clang also processes the example correctly, but objects in the list are never destroyed; this would cause a memory leak. ICC doesn't support initializer_list
at all.
Is my analysis correct?
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.
Back to 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>
, aninitializer_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 thestd::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
andv2
, theinitializer_list
object and array createdfor{ 1, 2, 3 }
have full-expression lifetime. Fori3
, the initializer_list object and array have automatic lifetime. — end example]
A little clarification about returning a braced-init-list
When you return a bare list enclosed in braces,
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.
This doesn't imply that the object returned to the calling scope is copied from something. For example, this is valid:
struct nocopy {
nocopy( int );
nocopy( nocopy const & ) = delete;
nocopy( nocopy && ) = delete;
};
nocopy f() {
return { 3 };
}
this is not:
nocopy f() {
return nocopy{ 3 };
}
Copy-list-initialization simply means the equivalent of the syntax nocopy X = { 3 }
is used to initialize the object representing the return value. This doesn't invoke a copy, and it happens to be identical to the 8.5.4/6 example of an array's lifetime being extended.
And Clang and GCC do agree on this point.
Other notes
A review of N2640 doesn't turn up any mention of this corner case. There has been extensive discussion about the individual features combined here, but I don't see anything about their interaction.
Implementing this gets hairy as it comes down to returning an optional, variable-length array by value. Because the std::initializer_list
doesn't own its contents, the function has to also return something else which does. When passing to a function, this is simply a local, fixed-size array. But in the other direction, the VLA needs to be returned on the stack, along with the std::initializer_list
's pointers. Then the caller needs to be told whether to dispose of the sequence (whether they're on the stack or not).
The issue is very easy to stumble upon by returning a braced-init-list from a lambda function, as a "natural" way to return a few temporary objects without caring how they're contained.
auto && il = []() -> std::initializer_list< noisydt >
{ return { noisydt{}, noisydt{} }; }();
Indeed, this is similar to how I arrived here. But, it would be an error to leave out the ->
trailing-return-type because lambda return type deduction only occurs when an expression is returned, and a braced-init-list is not an expression.
The wording you refer to in 8.5.4/6 is defective, and was corrected (somewhat) by DR1290. Instead of saying:
The lifetime of the array is the same as that of the
initializer_list
object.
... the amended standard now says:
The array has the same lifetime as any other temporary object (12.2 [class.temporary]), except that initializing an
initializer_list
object from the array extends the lifetime of the array exactly like binding a reference to a temporary.
Therefore the controlling wording for the lifetime of the temporary array is 12.2/5, which says:
The lifetime of a temporary bound to the returned value in a function return statement is not extended; the temporary is destroyed at the end of the full-expression in the return statement
Therefore the noisydt
objects are destroyed before the function returns.
Until recently, Clang had a bug that caused it to fail to destroy the underlying array for an initializer_list
object in some circumstances. I've fixed that for Clang 3.4; the output for your test case from Clang trunk is:
destroyed
destroyed
destroyed
received
destroyed
destroyed
destroyed
received
... which is correct, per DR1290.
这篇关于std :: initializer_list返回值的生命周期的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!