在成员初始值设定项中创建临时对象时销毁临时对象的要点

jac*_*k X 11 c++ gcc clang language-lawyer

#include <iostream>
struct A{
    A(int){
       
    }
    ~A(){
        std::cout<<"A destroy\n";
    }
};
struct B{
    B(int){
        std::cout<<"B construct\n";
    }
    ~B(){
        std::cout<<"B destroy\n";
    }
};
struct Content{
    A const& a;
};
struct Data{
    Data():c{0},b{0}{

    }
    Content c;
    B b;
};
int main(){
    Data d;
    std::cout<<"exit\n";
}
Run Code Online (Sandbox Code Playgroud)

GCC的输出是:

B construct
A destroy
exit
B destroy
Run Code Online (Sandbox Code Playgroud)

Clang 抱怨此代码格式错误。是两个编译器的性能。

关于Clang报的这个错误,标准里确实有相关的规定,就是:
[class.init#class.base.init-8]

绑定到内存初始值设定项中的引用成员的临时表达式格式错误。

我不确定 Clang 是否理解过度?在我看来,规则似乎是说由mem-initializer 的 mem-initializer-id命名的引用成员不应绑定到临时表达式。在我的示例中,c类的成员Data不是引用。

据推测,Clang可以认为任何使引用成员绑定到临时表达式的引用成员的初始化都发生在成员初始值设定项中都是格式错误的。所以我举了一个例子来检验是否Clang这么认为。

B construct
A destroy
exit
B destroy
Run Code Online (Sandbox Code Playgroud)

给出警告但不是错误。所以,我不确定Clang是这样认为的。不知道它是怎么理解规则的?

如果第一个例子本身有效,我会认为GCC不遵守标准。因为销毁临时对象的顺序。

[class.temporary#4]

临时对象被销毁作为评估完整表达式([intro.execution])(词法上)包含它们创建点的最后一步。

否则,临时对象将在作为成员初始值设定项的完整表达式的末尾被销毁,在我的示例中,即c{0}. 但是,GCCb构造子对象后销毁临时对象。我认为这是第一个问题。

实际上,临时被绑定的引用不是这里列出的异常:
[class.temporary#6]

引用绑定到的临时对象或作为引用绑定到的子对象的完整对象的临时对象在引用的生命周期内持续存在。此生命周期规则
例外是:

  • 在函数调用 ([expr.call]) 中绑定到引用参数的临时对象一直存在,直到包含调用的完整表达式完成。
  • 绑定到从括号表达式列表 ([dcl.init]) 初始化的类类型聚合的引用元素的临时对象一直存在,直到包含表达式列表的完整表达式完成。
  • 函数返回语句([stmt.return])中的返回值的临时绑定的生命周期没有延长;临时在 return 语句中的完整表达式结束时被销毁。
  • 一个临时绑定到一个 new-initializer ([expr.new]) 中的引用一直存在,直到包含 new-initializer 的完整表达式完成。

两者都不是,也就是说,我的第一个例子不是上面列表中列出的例外。因此,临时对象的生命周期应该与对象a的子对象的子对象cb生命周期相同,它们都与 的生命周期相同b。那么,为什么要GCC这么早销毁临时对象呢?临时对象不应该和bin的对象一起销毁main吗?我想这是第二个问题GCC。我不知道如何Clang处理临时对象,因为它之前给出了错误。

题:

  1. 这是Clang第一个示例报告错误吗?如果是对的, [class.init#class.base.init-8] 应该更清楚吗?

  2. 如果Clang过度理解[class.init#class.base.init-8],GCC销毁临时对象的性能算是bug吗?或者,exceptions遗漏了这个案例?即使异常忽略了这种情况(引用绑定发生在成员初始值设定项中),我仍然认为GCC是错误的,不应该c{0}在构造之前排序的 full-expression( )末尾销毁临时对象b

如何解释以上这些问题?

eca*_*mur 4

Clang 是正确的,但是,是的,标准可以更清晰。

[class.temporary]/6 (通过引用绑定延长临时表达式生命周期)的作用是确保除了列出的异常之外,绑定到引用的临时表达式的生命周期也延长到引用的生命周期。但是,当引用是类非静态数据成员时,引用的生命周期在绑定时并不是静态已知的(这发生在(可能默认的)构造函数中),因此临时延长使用寿命。由于非静态数据成员不包含在异常列表中,因此必须通过其他方式来防止这种情况,事实上,通过[class.base.init]中的 IF 情况来防止这种情况:

8 - 绑定到 mem-initializer 中的引用成员的临时表达式格式错误。

11 - 从默认成员初始值设定项绑定到引用成员的临时表达式格式不正确。

我们必须得出结论,这种语言的目的是使任何从类的(可能是默认的)构造函数中将临时绑定到类数据成员的尝试呈现格式错误,因为否则临时将有资格获得生命周期扩展,这将是无意义的(其中引用的生命周期是静态未知的)。因此,这必须包括子聚合的参考成员;该标准最好包含一个注释来明确这一点。

值得考虑将临时表达式绑定到引用,分为 3 种情况:

  1. IF 在 [class.base.init]/8 和 /11 下(类非静态数据成员)
  2. [class.temporary]/6 下的例外情况;临时变量在完整表达式结束时被销毁。
  3. 否则,寿命会延长。

因此,如果编译器不拒绝,并且代码不属于 [class.temporary]/6 中的异常之一,并且编译器不执行生命周期扩展(到引用的完整生命周期),则编译器有过错。

MSVC 也错误地接受您的代码并输出:

main
A(int)
~A()
B(int)
D()
exit
~B()
Run Code Online (Sandbox Code Playgroud)

一个有趣的情况是,当包含引用非静态数据成员的类是聚合时,因此有资格通过列表初始化语法进行聚合初始化(示例改编自CWG 1815):

struct A {};
struct C { A&& a = A{}; };
C c1;         // #1
C c2{A{}};    // #2
C c3{};       // #3
C c4 = C();   // #4
Run Code Online (Sandbox Code Playgroud)

尽管 gcc 和 MSVC 错误地接受,但这里#1和的格式不正确。根据 [class.base.init]/11 标准的当前措辞,格式不正确,但这违背了CWG 1815中指出的委员会的意图:#4#3

2014 年 2 月会议记录:

CWG 同意建议的方向,它将像 #2 一样对待示例中的#3,并删除默认构造函数

也就是说,#3将是有效的,并导致寿命延长;gcc 和 clang 这样做,但 MSVC 无法在或中执行生命周期延长;icc仅对执行生命周期延长#2#3#2。奇怪的是,clang 报告了一个诊断,声称它不会执行生命周期延长,尽管这样做了!:

警告:抱歉,不支持使用默认成员初始值设定项通过聚合初始化创建的临时对象的生命周期扩展;临时的生命周期将在完整表达式结束时结束 [-Wdangling]

我已经询问了聚合初始化中 mem-initializer 的有效性和/或生命周期扩展

请注意(假设这是允许的)它只能工作,因为它c3是一个完整的对象;如果它是一个成员对象(如c第一个示例中所示),[class.base.init] 将明确适用并且c3格式错误。