背景
对于用户及编译器来说,预先知道某个函数不会抛出异常是大有好处的。
- 首先,知道函数不会抛出异常有助于简化调用该函数的代码
- 其次,如果编译器确认函数不会出异常,它就能执行某些特殊的优化操作,而这些优化操作并不适用于可能出错的代码。
noexcept异常说明
在C++11新标准中,我们可以通过提供noexcept说明指定某个函数不会抛出异常。
其形式是关键字 noexcept 紧跟在函数的参数列表后面,用以标识该函数不会抛出异常:
void recoup(int) noexcept //不会抛出异常
void alloc(int); //可能抛出异常
这两条声明语句指出 recoup 将不会抛出任何异常,而alloc可能抛出异常。
我们说recoup做了不抛出说明(nonthrowingspecification)。
出现的位置
对于一个函数来说,noexcept说明要么出现在该函数的所有声明语句和定义语句中,要么一次也不出现。
void A()noexcept;
int main()
{
}
void A()//这是错误的
{
}
该说明应该在函数的尾置返回类型之前
auto A() -> int noexcept//错误
{
}
auto A() -> noexcept int//错误
{
}
auto A() noexcept-> int//正确
{
}
我们也可以在函数指针的声明和定义中指定noexcept
int(*d)(int)noexcept = 0;
在typedef或类型别名中则不能出现noexcept。
typedef int(*AAA)(int)noexcept;
在成员函数中,noexcept说明符需要跟在const及引用限定符之后,在final、override或虚函数的=0之前。
class BB
{
public:
void A()const noexcept;//正确
void A()noexcept const;//错误
void B() && noexcept;//正确
void B()noexcept&& ;//错误
void C() & noexcept;//正确
void C()noexcept&;//错误
void D()final noexcept;//错误
void D()noexcept final;//正确
void E()override noexcept;//错误
void E()noexcept override;//正确
virtual void F() = 0 noexcept;//错误
virtual void F()noexcept = 0;//正确
};
违反异常说明
读者需要清楚的一个事实是编译器并不会在编译时检查 noexcept 说明。
实际上,如果一个函数在说明了noexcept的同时又含有throw语句或者调用了可能抛出异常的他函数,编译器将顺利编译通过,并不会因为这种违反异常说明的情况而报错(不排除别编译器会对这种用法提出警告):
// 尽管该函数明显违反了异常说明,但它仍然可以顺利编译通过
void f()noexcept //承诺不会抛出异常
{
throw exception();//违反了异常说明
}
因此可能出现这样一种情况:尽管函数声明了它不会抛出异常,但实际上还是抛出了。
一旦一个noexcept函数抛出了异常,程序就会调用 terminate以确保遵守不在运行时出异常的承诺。
上述过程对是否执行栈展开未作约定,因此noexcept可以用在两种情况下:
- 一是我们确认函数不会抛出异常
- 二是我们根本不知道该如何处理异常
指明某个函数不会抛出异常可以令该函数的调用者不必再考虑如何处理异常。无论是函数确实不抛出异常,还是程序被终止,调用者都无须为此负责。
通常情况下,编译器不能也不必在编译时验证异常说明。
terminate函数
std::terminate函数是C++标准库中的一个函数,它在遇到无法处理的异常或者违反某些运行时约束(比如,在不允许抛出异常的地方抛出了异常)时会被调用。一旦std::terminate被调用,程序就会终止执行,并且不会返回到调用它的地方。
这个函数通常不会被我们直接调用,而是由C++的运行时系统在某些错误情况下自动调用的。但是,你可以通过std::set_terminate函数来设置你自己的终止处理函数,这个函数会在std::terminate被调用时被执行。
简单来说,std::terminate就像是程序的一个紧急出口,当程序遇到无法处理的严重问题时,就会通过这个出口来结束程序的运行。
调用场景
std::terminate的调用场景通常是在程序遇到无法恢复的错误时。这种情况可能包括以下几种:
- 未捕获的异常:当程序抛出一个异常,并且没有相应的catch块来捕获这个异常时,std::terminate可能会被调用。这是为了防止程序因为未处理的异常而进入不稳定的状态。
- 违反运行时约束:在某些情况下,程序可能会违反C++的运行时约束,比如尝试在不允许抛出异常的上下文中抛出异常。这时,std::terminate也可能被调用。
- 显式调用:虽然不常见,但开发者也可以选择直接调用std::terminate来终止程序。这通常是在检测到无法恢复的错误或条件时,作为一种紧急停止机制。
当std::terminate被调用时,它会首先调用当前安装的terminate_handler。默认的terminate_handler会调用std::abort来结束程序。
但开发者可以通过std::set_terminate来设置自己的terminate_handler,以便在程序终止前执行一些清理工作或记录错误信息。
需要注意的是,std::terminate的调用通常意味着程序无法继续正常运行,因此应该尽量避免到达这种情况。在编写代码时,应该尽可能地处理所有可能的异常和错误情况,以确保程序的稳定性和可靠性。
set_terminate()函数
通过使用标准的set_terminate()函数,可以设置自己的terminate()函数。自定义的terminate()函数不能有参数,而且返回值类型为void。另外,terminate函数不能返回也不能抛出异常,它必须终止程序。如果terminate函数被调用,这就意味着问题已经无法解决了。
- 参数类型为 void(*)();
- 函数指针,没有参数、没有返回值;
- 返回值为默认的 terminate() 函数入口地址;
例如:
#include <iostream>
#include <cstdlib>
#include <exception> // C++ 标准库中与异常相关的头文件;
using namespace std;
void my_terminate()
{
cout << "void my_terminate()" << endl;
exit(); // 结束当前的程序;可以确保所有的全局对象和静态局部对象全部都正常析构;
// abort(); // “已放弃”是这个函数打印出来的,这个函数是异常终止一个程序,并且异常终止的时候不会调用任何对象的析构函数;
}
class Test
{
public:
Test()
{
cout << "Test()";
cout << endl;
}
~Test()
{
cout << "~Test()";
cout << endl;
}
};
int main()
{
set_terminate(my_terminate);
static Test t;
throw ;
return ;
}
老版本
早期的C++版本设计了一套更加详细的异常说明方案,该方案使得我们可以指定某个函数可能抛出的异常类型。函数可以指定一个关键字throw,在后面跟上括号括起来的异常类型列表。throw说明待所在的位置与新版本C++中noexcept所在的位置相同。
上述使用throw的异常说明方案在C++11新版本中已经被取消了。
然而尽管如此,它还有一个重要的用处。如果函数被设计为是 throw()的,则意味着该函数将不会抛出异常:
void recoup(int) noexcept; // recoup不会抛出异常 void recoup(int) throw() /等价的声明
面的两条声明语句是等价的,它们都承诺recoup不会抛出异常。
异常说明的实参
noexcept说明符接受一个可选的实参,该实参必须能转换为bool类型:
- 如果实参是true,则函数不会抛出异常;
- 如果实参是false,则函数可能抛出异常:
void recoup(int) noexcept(true); // recoup 不会抛出异常
void alloc(int) noexcept(false); // alloc可能抛出异常
noexcept 运算符
noexcept说明符的实参常常与noexcept运算符混合使用。
noexcept运算符是一个一元运算符,它的返回值是一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。和sizeof类似,noexcept也不会求其运算对象的值。
例如,因为我们声明recoup时使用了noexcept说明符,所以下面的表达式的返
回值为true:
noexcept(recoup(i))// 如果recoup不抛出异常则结果为true;否则结果为false
更普通的形式是:
noexcept (e);
当e调用的所有函数都做了不抛出说明且e本身不含有 throw语句时,上述表达式为true;否则noexcept (e)返回false。
我们可以使用 noexcept运算符得到如下的异常说明:
void f() noexcept (noexcept(g()));//f和g的异常说明一致
如果函数g承诺了不会抛出异常,则f也不会抛出异常;如果g没有异常说明符,或者g虽然有异常说明符但是允许抛出异常,则f也可能抛出异常。
我们再看看这个
int A()noexcept(false)
{}
int A()
{}
//这两个函数是等价的
这两个函数都代表可能抛出异常
noexcept有两层含义:
- 当跟在函数参数列表后面时它是异常说明符;
- 当作为noexcept异常说明的bool实参出现时,它是一个运算符。
异常说明与指针、虚函数和拷贝控制
异常与函数指针
尽管noexcept 说明符不属于函数类型的一部分,但是函数的异常说明仍然会影响函数的使用。
函数指针及该指针所指的函数必须具有一致的异常说明。
也就是说,
- 如果我们为某个指针做了不抛出异常的声明,则该指针将只能指向不抛出异常的函数。
- 相反,如果我们显式或隐式地说明了指针可能抛出异常,则该指针可以指向任何函数,即使是承诺了不抛出异常的函数也可以:
int A1(int a) { cout << "使用中" << endl; return 3; } int A2(int a)noexcept { cout << "使用中" << endl; return 3; } int main() { int(*a)(int)noexcept = A1;//错误 int(*b)(int)noexcept = A2;//正确 int(*a2)(int)= A1;//正确 int(*b2)(int) = A2;//正确 }
异常与虚函数
- 如果一个虚函数承诺了它不会抛出异常,则后续派生出来的虚函数也必须做出同样的承诺;
- 与之相反,如果基类的虚函数允许抛出异常,则派生类的对应函数既可以允许抛出异常,也可以不允许抛出异常;
class Base { public: virtual double fl (double) noexcept; //不会抛出异常 virtual int f2() noexcept(false);//可能抛出异常 virtual void f3(); //可能抛出异常 } class Derived : public Base { public: double f1(double);//错误:Base::fl承诺不会抛出异常 int f2() noexcept (false);// 正确:与Base::f2的异常说明一致 void f3() noexcept;//正确:Derived的f3做了更严格的限定,这是允许的 }
异常与拷贝控制
当编译器合成拷贝控制成员(构造函数,拷贝构造函数,拷贝控制运算符,移动构造函数,移动构造运算符)时,同时也生成一个异常说明。
- 如果对所有成员和基类的所有操作都承诺了不会抛出异常,则合成的成员是noexcept的。
- 如果合成成员调用的任意一个函数可能抛出异常,则合成的成员是noexcept(false)。
而且,如果我们定义了一个析构函数但是没有为它提供异常说明,则编译器将合成一个。
合成的异常说明将与假设由编译器为类合成析构函数时所得的异常说明一致。