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


#include <iostream>
struct A{
A(int){

}
~A(){
std::cout<<"A destroyn";
}
};
struct B{
B(int){
std::cout<<"B constructn";
}
~B(){
std::cout<<"B destroyn";
}
};
struct Content{
A const& a;
};
struct Data{
Data():c{0},b{0}{
}
Content c;
B b;
};
int main(){
Data d;
std::cout<<"exitn";
}

GCC的输出为:

B construct
A destroy
exit
B destroy

Clang抱怨此代码格式不正确。以下是两个编译器的性能。

关于Clang报告的错误,标准中确实有一条相关规则,即:
[class.init#class.base.init-8]

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

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

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

struct A{
int const& rf;
};
struct B{
B():a(new A{0}){}
A* a;
};
int main(){
B b;
delete b.a;
}

它会发出警告,但不会出错。所以,我不确定Clang是这么认为的。我不知道它是如何理解规则的?

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

[临时#4级]

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

否则,临时对象将在完整表达式的末尾被销毁,该表达式是成员初始值设定项,在我的示例中是c{0}。然而,GCC在子对象b已经构造之后破坏临时对象。我认为这是第一个问题。

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

绑定引用的临时对象或作为绑定引用的子对象的完整对象的临时对象在引用的生存期内保持
此生存期规则的异常为:

  • 绑定到函数调用([expr.call])中引用参数的临时对象将一直存在,直到包含该调用的完整表达式完成为止
  • 绑定到从带括号的表达式列表([dcl.init])初始化的类类型聚合的引用元素的临时对象将持续存在,直到包含表达式列表的完整表达式完成为止
  • 函数返回语句([stmt.return])中返回值的临时绑定的生存期未延长;临时在return语句中的完整表达式末尾被销毁
  • 到新初始值设定项([expr.new])中引用的临时绑定将持续存在,直到包含新初始值设置项的完整表达式完成为止

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

问题:

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

  2. 如果Clang过度理解了[class.init#class.base.int-8],那么GCC破坏临时对象的性能是否被认为是一个bug?或者,exceptions遗漏了这个案例?即使例外情况忽略了这种情况(引用绑定发生在成员初始值设定项中),我仍然认为GCC有错误,不应该在构建b之前测序的完整表达式(c{0})的末尾破坏临时性吗。

如何解释上述问题?

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

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

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

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

我们必须得出结论,这种语言的目的是将格式错误的从类的(可能是默认的)构造函数中将临时绑定到类数据成员的任何尝试渲染出来,因为否则临时将有资格获得生存期扩展,这将是无意义的(引用的生存期不是静态已知的)。因此,这必须包括子集合的参考成员;本标准应包含一份说明,明确说明这一点。

值得考虑的是,临时表达式与引用的绑定属于以下三种情况:

  1. [class.base.init]/8和/11下的IF(类非静态数据成员)
  2. [临时类别]/6项下的例外情况;临时在完整表达式的末尾被销毁
  3. 否则,将延长使用寿命

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

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

main
A(int)
~A()
B(int)
D()
exit
~B()

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

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

这里#1#4是不正确的,尽管gcc和MSVC不正确地接受。#3不符合【class.base.init】/11标准的现行措辞,但这与CWG 1815:中指出的委员会意图相反

2014年2月会议纪要:

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

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

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

我在聚合初始化中询问了mem初始值设定项的有效性和/或生存期扩展。

请注意(假设这是允许的)它只起作用,因为c3是一个完整的对象;如果它是一个成员对象(如第一个示例中的c)[class.base.init]将最终应用,c3也将是格式错误的。

最新更新