本文可以看作老外写的一篇文章的带我个人理解的翻译。该文章链接是 http://www.gotw.ca/gotw/066.htm
TL;DR
C++ 的构造和析构函数都可能会抛异常。总的来说需要遵守以下的原则:
- 构造函数处理初始化的 try-block 最多只有两个功能:转换 Exception,输出一些信息(比如 print 原来的错误是什么)
- 析构函数抛出异常只可能让程序终止:C++ 默认析构函数是 noexcpet 的,所以在析构函数里抛异常会直接 std::terminate
- 裸指针必须在构造函数的函数体初始化:如果放在初始化列表,你永远都不知道它是否是野指针
- 裸指针必须在构造函数函数体内 new、析构函数的函数体内 delete,永远不要尝试在构造函数处理初始化的 try-block 中释放,理由同 3
- 如果构造函数有异常规范,那么该类必须允许处理其父类、成员构造函数抛出的异常类型
- 如果允许某个成员未成功初始化,那你得要用指针
- 多用 RAII
上述的“裸指针”,原文是"unmanaged resourses",所以实际意思远远不止裸指针,这里翻译为裸指针是方便理解的,毕竟“未受管理的资源”这个概念比较抽象,我能想到的最具体、最为大众所熟知的就是裸指针,其他的比如某个资源的 id 等等。
至于为什么不是 smart_ptr,其实是 smart_ptr 转换指针为一个带有构造、析构的对象了。
需要解释的主要是 1、5
一个对象的生命周期
一个对象的生命周期从构造函数正常 return 的时候开始,到调用析构函数的那一刻结束。所以在构造函数抛异常的时候是不会自动调用析构函数的——它的生命周期都还没开始,就好像你要杀一个死人一样。
构造函数初始化列表异常处理
构造函数初始化列表处理异常有特殊的语法:
#include <iostream>
class A {
public:
A() { throw "A"; }
};
class B {
public:
B() { throw "B"; }
};
class C: public A {
public:
C() try: A{}, b{} {
} catch (...) {
std::cout<<"C's Ctor failed\n";
}
B b;
};
int main() {
C{};
return 0;
}
但是这样对象能构造完成吗?不会,你会发现它自动 rethrow 了异常。
你也可以手动新 throw 一个异常
所以构造函数初始化列表的异常,只能够“转换一下异常”,或者说输出别的信息。
C++ 异常规范
早期 C++ 可以在函数中声明它会抛出什么异常(C++11 可以,但是会有警告,C++17 后就不可以了)。现在可以直接声明一个函数是 noexcept 的。
“允许抛出父类抛出的异常类型”,意思是子类构造函数必须标记它可能会抛出父类的异常类型。用代码表示是这样的:
C::C() 标记了它只可能抛出 const int
类型异常,但是实际上其父类抛出 const char *
。所以就算有 try-catch,程序还是异常退出了,而不是输出 "C'ctor faied"。