本文可以看作老外写的一篇文章的带我个人理解的翻译。该文章链接是 http://www.gotw.ca/gotw/066.htm

TL;DR

C++ 的构造和析构函数都可能会抛异常。总的来说需要遵守以下的原则:

  1. 构造函数处理初始化的 try-block 最多只有两个功能:转换 Exception,输出一些信息(比如 print 原来的错误是什么)
  2. 析构函数抛出异常只可能让程序终止:C++ 默认析构函数是 noexcpet 的,所以在析构函数里抛异常会直接 std::terminate
  3. 裸指针必须在构造函数的函数体初始化:如果放在初始化列表,你永远都不知道它是否是野指针
  4. 裸指针必须在构造函数函数体内 new、析构函数的函数体内 delete,永远不要尝试在构造函数处理初始化的 try-block 中释放,理由同 3
  5. 如果构造函数有异常规范,那么该类必须允许处理其父类、成员构造函数抛出的异常类型
  6. 如果允许某个成员未成功初始化,那你得要用指针
  7. 多用 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 了异常。

image

你也可以手动新 throw 一个异常

image

所以构造函数初始化列表的异常,只能够“转换一下异常”,或者说输出别的信息。

C++ 异常规范

早期 C++ 可以在函数中声明它会抛出什么异常(C++11 可以,但是会有警告,C++17 后就不可以了)。现在可以直接声明一个函数是 noexcept 的。

“允许抛出父类抛出的异常类型”,意思是子类构造函数必须标记它可能会抛出父类的异常类型。用代码表示是这样的:

image

C::C() 标记了它只可能抛出 const int 类型异常,但是实际上其父类抛出 const char *。所以就算有 try-catch,程序还是异常退出了,而不是输出 "C'ctor faied"。