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

Posted

技术标签:

【中文标题】std::initializer_list 返回值的生命周期【英文标题】:lifetime of a std::initializer_list return value 【发布时间】:2013-02-23 13:15:03 【问题描述】:

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:

带有 braced-init-list 的 return 语句通过指定初始化列表中的复制列表初始化 (8.5.4) 初始化要从函数返回的对象或引用。 p>

8.5.4/1:

…复制初始化上下文中的列表初始化称为copy-list-initialization

8.5/14:

T x = a; ... 形式发生的初始化称为复制初始化

回到 8.5.4/3:

类型 T 的对象或引用的列表初始化定义如下:……

— 否则,如果 T 是 std::initializer_list&lt;E&gt; 的特化,则如下所述构造 initializer_list 对象,并用于根据从相同类型的类中初始化对象的规则(8.5 )。

8.5.4/5:

std::initializer_list&lt;E&gt; 类型的对象是从初始化列表构造的,就好像实现分配了一个 N 类型 E 元素的数组,其中 N 是初始化列表中的元素数。该数组的每个元素都使用初始值设定项列表的相应元素进行复制初始化,并且构造 std::initializer_list&lt;E&gt; 对象以引用该数组。如果需要进行窄化转换来初始化任何元素,则程序格式错误。

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 ;
 

对于v1v2,为initializer_list 创建的initializer_list 对象和数组具有完整的表达式生命周期。对于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 在这一点上做 agree。


其他说明

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

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

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

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

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

【问题讨论】:

receive 调用之前 调用receive 时,GCC 生成的“已破坏”消息难道不是 @987654354 中的对象的表现吗? @函数被破坏?毕竟,你是按价值传递的。在这种情况下,这不会是错误的。 Clang 可能会对此进行优化。 我在 LWS 示例中添加了更多 std::cout。 Weird Output。我期待在----1 之前有6 个destroyed:在received 之前有3 个,在它之后有3 个。为问题 +1。 @jogojapan 我将输出添加到复制构造函数,但没有一个实现调用它。我认为这里没有noisydt 的复制空间。请注意,复制初始化列表不会复制底层数组。 Still Weird Output。 first received 之后但----1 之前没有destroyed @Nawaz 因为它破坏了整个数组;没有什么可摧毁的了。没有副本。在野外,“接收”会产生段错误,因为被破坏的对象是std::string 【参考方案1】:

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

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

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

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

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

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

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

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

destroyed
destroyed
destroyed
received
destroyed
destroyed
destroyed
received

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

【讨论】:

优秀。我认为如果将其表述为“延长数组的生命周期,就好像initializer_list 是引用数组类型一样”会更清楚。将列表对象与引用等同起来有点像跳过一个心理圈。无论如何,整个事情都被打破了。 initializer_list 及其 *begin()*end() 的名称应永久为右值,如果用户希望保留一个,则应将其声明为 const,因此不会选择典型的移动构造函数。 所以返回initializer_list的函数的唯一用例是返回static @MattMcNabb:您也可以合理地返回一个作为参数给出的参数。但是返回initializer_list 应该被怀疑。 @RichardSmith 对。我想知道是否应该完全禁止它,因为有效的用例是深奥的。常见的编译器不会警告这个问题,但也许他们应该发出警告。 更新:大约在上周(2018 年 7 月下旬),Clang 主干现在发出警告,并显示“本地临时对象的返回地址”错误。【参考方案2】:

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

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

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

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

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

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

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

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

【讨论】:

数组的生命周期与initializer_list (8.5.4/6) 相同。 initializer_list 是返回值对象 (6.6.3/2)。该对象绑定到一个命名引用,并且它的生命周期被延长,与 ScopeGuard 习惯用法 (12.2/5) 相同。因此阵列的寿命延长了。您的反例是 12.2/5 中列出的例外情况之一。 (您肯定知道这一点。) 1290 似乎保留了该行为,因为该数组仍在初始化/绑定到调用范围内可见的原始临时对象。 1599 直截了当地提出了这个问题,但没有提出更改建议。 12.2/5 的第三个项目符号是否不适用于您的情况?临时数组绑定到返回的initializer_list,但在完整表达式的末尾被销毁,即在您调用receive(il) 之前。构造函数的例子是“重新绑定”不会“重新延长”生命周期的一个例子,(我相信)你的例子是另一种情况。虽然我很高兴被证明是错误的,但我尽量避免对 initializer_list 对象做这样的事情,因为我不相信他们会做你想做的事:) 不,那是指返回一个引用。 obj const &amp;f() return obj(); 根据该规则返回一个悬空引用,但否则将是有效的。我的情况与 ScopeGuard 成语相同。函数返回对象临时的;没有暂时的约束。完善语言的唯一方法是挑战极限……我不相信这一点,因为 1599,或者我对标准中“歧义”的另一种解读。也许我会尝试添加对 GCC 的支持,这将是一个很好的挑战,但几乎可以肯定它不适合 ABI(或者可能带有堆作弊)。 initializer_list是函数返回对象,底层数组是临时的 这是怎么回事?阵列的状态处于一种不确定状态。我们只知道它的生命周期与initializer_list 相同。

以上是关于std::initializer_list 返回值的生命周期的主要内容,如果未能解决你的问题,请参考以下文章

为啥在使用大括号初始值设定项列表时首选 std::initializer_list 构造函数?

使用 std::initializer_list 创建指向 std::min 的函数指针

为啥 `std::initializer_list` 不提供下标运算符?

为啥 std::initializer_list 不是内置语言?

std::initializer_list 作为函数参数

如何将 C 数组转换为 std::initializer_list?