【发布时间】:2021-04-03 07:47:40
【问题描述】:
#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";
}
GCC的输出是:
B construct
A destroy
exit
B destroy
Clang 抱怨这段代码格式不正确。 Here 是两个编译器的性能。
关于Clang报错,标准中确实有相关规定,即:
[class.init#class.base.init-8]
绑定到 mem-initializer 中的引用成员的临时表达式格式不正确。
我不确定 Clang 是否理解过度了?在我看来,规则似乎是说,由 mem-initializer 的 mem-initializer-id 命名的引用成员不应绑定到临时表达式。在我的示例中,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;
}
gives 是警告,但不是错误。所以,我不确定Clang 是否这么认为。不知道它是怎么理解规则的?
如果第一个例子本身是有效的,我会认为GCC 不符合标准。因为销毁临时对象的顺序。
临时对象在评估完整表达式 ([intro.execution]) 的最后一步被销毁,该完整表达式 (lexically) 包含它们的创建点。
临时对象将在成员初始化器的完整表达式末尾被销毁,在我的示例中,即c{0}。但是,GCC 会在子对象b 构造完成后销毁临时对象。我认为这是第一个问题。
其实临时绑定的引用也不例外:
[class.temporary#6]
引用绑定到的临时对象或作为引用绑定到的子对象的完整对象的临时对象在引用的生命周期内持续存在
此生命周期规则的例外是:
- 在函数调用 ([expr.call]) 中绑定到引用参数的临时对象一直存在,直到包含调用的完整表达式完成为止。
- 绑定到从带括号的表达式列表 ([dcl.init]) 初始化的类类型聚合的引用元素的临时对象将持续存在,直到包含表达式列表的完整表达式完成为止。
- 临时绑定到函数返回语句 ([stmt.return]) 中的返回值的生命周期未延长;临时在 return 语句中的完整表达式的末尾被销毁。
- 在 new-initializer ([expr.new]) 中的引用的临时绑定一直存在,直到包含 new-initializer 的完整表达式完成为止。
也不是,也就是说,我的第一个例子不是上面列表中列出的例外。因此,临时对象的生命周期应该与对象b的子对象c的子对象a的生命周期相同,它们的生命周期都与b的生命周期相同。那么,为什么GCC 这么早就销毁临时对象呢?临时对象不应该和main中的对象b一起销毁吗?我想这是GCC的第二期。我不知道Clang如何处理临时对象,因为它之前已经报错了。
问题:
-
Clang报告第一个示例的错误吗?如果是正确的,[class.init#class.base.init-8] 是否应该更清楚? -
如果
Clang过度理解[class.init#class.base.init-8],那么GCC销毁临时对象的表现是否算是bug?或者,exceptions忽略了这个案例?即使异常省略了这种情况(引用绑定发生在成员初始化器中),我仍然认为GCC有错误,临时不应该在完整表达式(c{0})的末尾被销毁,这是在构造之前排序的b.
如何解读以上这些问题?
【问题讨论】:
-
这是导致问题的
c{0}-c的a成员使用对临时A对象的引用进行初始化。 -
@1201ProgramAlarm 这会引起什么问题吗?是否违反任何规则?
c{0}将执行聚合初始化。这里的关键点是临时A对象的生命周期 -
@1201ProgramAlarm 我不确定你是否引用了this example。这种情况也不例外,它具有销毁临时对象的正确行为。
标签: c++ gcc clang language-lawyer