【发布时间】:2020-09-18 03:44:57
【问题描述】:
#include <iostream>
struct A {
A(int id):id_(id){
std::cout<<"construct A with id: "<<id_<<"\n";
}
int id_;
~A(){
std::cout<<"destory A with id: "<< id_<<"\n";
}
A(A const&) = default;
};
struct Y {
~Y() noexcept(false) {
std::cout<<"destory Y\n";
throw 0;
}
};
A f() {
try {
A a(1);
Y y;
A b(2);
return {3}; // #1
} catch (...) {
std::cout<<"handle exception\n";
}
A dd(4);
return {5}; // #2
}
int main(){
auto t = f();
std::cout<<"in main\n";
}
它的outcomes 是(GCC 和 Clang 给出相同的结果):
construct A with id: 1
construct A with id: 2
construct A with id: 3
destory A with id: 2
destory Y
destory A with id: 1
handle exception
construct A with id: 4
construct A with id: 5
destory A with id: 4
in main
destory A with id: 5
考虑一下这个例子,它是except.ctor#2的一个变体例子,我对这个例子及其在标准中对应的注释有很多疑问,即:
在#1 处,构造了返回的类型 A 的对象。然后,局部变量 b 被销毁([stmt.jump])。接下来,局部变量y被销毁,导致堆栈展开,导致返回的对象被销毁,接着是局部变量a的销毁。最后在#2处再次构造返回的对象。
首先,在#1,为什么要创建类型 A 的对象?关于return statement 的规则说:
return 语句通过从操作数复制初始化来初始化(显式或隐式)函数调用的泛左值结果或纯右值结果对象。
而return statement的操作数是:
return 语句的 expr-or-braced-init-list 称为其操作数。
这意味着 #1 处的大括号初始化列表 {3} 是操作数,调用的结果将由该操作数复制初始化,最后两个打印出来的句子证明了这一观点。
好吧,即使我同意#1 和#2 将创建这些A 类型的临时对象,但是我不同意临时对象和局部变量的销毁顺序,我的观点return statement 规则是这样说的:
调用结果的复制初始化在return语句的操作数建立的完整表达式末尾处的临时对象销毁之前排序,反过来,在销毁之前排序包含 return 语句的块的局部变量([stmt.jump])。
IIUC,由return statement 的操作数创建的这些临时变量的销毁应该在这些局部变量的销毁之前进行排序。那么,为什么注释说“接下来,局部变量 y 被破坏,导致堆栈展开,导致返回的对象被破坏”?根据上面的规则,临时变量的销毁应该先于局部变量y的销毁,异常规则说:
自进入 try 块以来,为每个已构造、但尚未销毁的类类型自动对象调用析构函数。
此时,即y的销毁,return statement的操作数创建的临时对象已经销毁了,不是吗?
并且永远不会评估带有id 3 的对象的破坏,但是这个问题已经在其他 SO 问题中提出过,这个问题不是我的问题的主题。
这个例子我真的看不懂,这些问怎么解释?
【问题讨论】:
标签: c++ exception c++17 language-lawyer