[TOC]

文章转自:https://www.ethsonliu.com/2020/04/noexcept.html

概述

C++98 中的异常规范(Exception Specification)

throw 关键字除了可以用在函数体中抛出异常,还可以用在函数头和函数体之间,指明当前函数能够抛出的异常类型,这称为异常规范,有些教程也称为异常指示符或异常列表。请看下面的例子:

1
double func1 (char param) throw(int);

函数 func1 只能抛出 int 类型的异常。如果抛出其他类型的异常,try 将无法捕获,并直接调用 std::unexpected

如果函数会抛出多种类型的异常,那么可以用逗号隔开,

1
double func2 (char param) throw(int, char, exception);

如果函数不会抛出任何异常,那么只需写一个空括号即可,

1
double func3 (char param) throw();

同样的,如果函数 func3 还是抛出异常了,try 也会检测不到,并且也会直接调用 std::unexpected

虚函数中的异常规范

C++ 规定,派生类虚函数的异常规范必须与基类虚函数的异常规范一样严格,或者更严格。只有这样,当通过基类指针(或者引用)调用派生类虚函数时,才能保证不违背基类成员函数的异常规范。请看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base
{
public:
virtual int fun1(int) throw();
virtual int fun2(int) throw(int);
virtual string fun3() throw(int, string);
};

class Derived: public Base
{
public:
int fun1(int) throw(int); //错!异常规范不如 throw() 严格
int fun2(int) throw(int); //对!有相同的异常规范
string fun3() throw(string); //对!异常规范比 throw(int, string) 更严格
}

异常规范与函数定义和函数声明

C++ 规定,异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致,不能更加严格或者更加宽松。请看下面的几组函数:

1
2
3
4
5
6
7
8
9
10
11
// 错!定义中有异常规范,声明中没有
void func1();
void func1() throw(int) { }

// 错!定义和声明中的异常规范不一致
void func2() throw(int);
void func2() throw(int, bool) { }

// 对!定义和声明中的异常规范严格一致
void func3() throw(float, char*);
void func3() throw(float, char*) { }

异常规范在 C++11 中被摒弃

异常规范的初衷是好的,它希望让程序员看到函数的定义或声明后,立马就知道该函数会抛出什么类型的异常,这样程序员就可以使用 try-catch 来捕获了。如果没有异常规范,程序员必须阅读函数源码才能知道函数会抛出什么异常。

不过这有时候也不容易做到。例如,func_outer() 函数可能不会引发异常,但它调用了另外一个函数 func_inner(),这个函数可能会引发异常。再如,编写的一个函数调用了老式的一个库函数,此时不会引发异常,但是老式库更新以后这个函数却引发了异常。

其实,不仅仅如此,

  1. 异常规范的检查是在运行期而不是编译期,因此程序员不能保证所有异常都得到了 catch 处理。

  2. 由于第一点的存在,编译器需要生成额外的代码,在一定程度上妨碍了优化。

  3. 模板函数中无法使用。比如下面的代码,

    1
    2
    3
    4
    5
    6
    template<class T>
    void func(T k)
    {
    T x(k);
    x.do_something();
    }

    赋值函数、拷贝构造函数和 do_something() 都有可能抛出异常,这取决于类型 T 的实现,所以无法给函数 func 指定异常类型。

  4. 实际使用中,我们只需要两种异常说明:抛异常和不抛异常,也就是 throw(…) 和 throw()。

所以 C++11 摒弃了 throw 异常规范,而引入了新的异常说明符 noexcept。

C++11 noexcept

noexcept 紧跟在函数的参数列表后面,它只用来表明两种状态:”不抛异常” 和 “抛异常”。

1
2
3
4
5
void func_not_throw() noexcept; // 保证不抛出异常
void func_not_throw() noexcept(true); // 和上式一个意思

void func_throw() noexcept(false); // 可能会抛出异常
void func_throw(); // 和上式一个意思,若不显示说明,默认是会抛出异常(除了析构函数,详见下面)

对于一个函数而言,

  1. noexcept 说明符要么出现在该函数的所有声明语句和定义语句,要么一次也不出现。
  2. 函数指针及该指针所指的函数必须具有一致的异常说明。
  3. 在 typedef 或类型别名中则不能出现 noexcept。
  4. 在成员函数中,noexcept 说明符需要跟在 const 及引用限定符之后,而在 final、override 或虚函数的 =0 之前。
  5. 如果一个虚函数承诺了它不会抛出异常,则后续派生的虚函数也必须做出同样的承诺;与之相反,如果基类的虚函数允许抛出异常,则派生类的虚函数既可以抛出异常,也可以不允许抛出异常。

需要注意的是,编译器不会检查带有 noexcept 说明符的函数是否有 throw

1
2
3
4
void func_not_throw() noexcept
{
throw 1; // 编译通过,不会报错(可能会有警告)
}