C++11-noexcept
1.noexcept
在C++11中,noexcept
关键字有两种主要用法:作为异常说明符(Exception Specifier)和作为运算符(Operator)。下面我们来研究这两种用法。
2.noexcept作为异常说明符
在异常处理的代码中,我们可能会看到如下的异常声明表达式:
void excpt_func() throw(int, double) { ... }
在excpt_func
函数声明之后,我们定义了一个动态异常生命throw(int, double)
,该声明指出了excpt_func
可能抛出的异常类型。实际上,该特性现在很少被使用,在C++中被弃用了,而表示函数不会抛出异常的动态异常生命throw()
也被新的noexcept
所替代。
noexcept
表示其声明的函数不会抛出异常。在C++11中如果noexcept
声明的函数抛出了异常,编译器可以选择直接调用std::terminate()
函数来终止程序的运行。
noexcept
作为异常说明符有两种形式,一种就是简单地在函数生命后加上noexcept
关键字。如:
void except_func() noexcept;
另一种则是接受一个常量表达式作为参数,如:
void except_func() noexcept(常量表达式);
常量表达式的结果会被转换成一个bool类型的值。该值为true,表示函数不会抛出异常,反之,则有可能抛出异常。这里,不带常量表达式的noexcept相当于声明了noexcept(true)
,即不会抛出异常。
在通常情况下,C++11中使用noexcept
可以有效阻止异常的传播与扩散。如以下例程:
#include <iostream>
using namespace std;
void Throw() { throw 1; }
void NoBlockThrow() { Throw(); }
void BlockThrow() noexcept { Throw(); }
int main() {
try {
Throw();
}
catch(...) {
cout << "Found throw." << endl;
}
try {
NoBlockThrow();
}
catch(...) {
cout << "Throw is not blocked. " << endl;
}
try {
BlockThrow();
}
catch (...) {
cout << "Found throw 1." << endl; //terminate called after throwing an instance of 'int'
}
}
//编译选项:g++ -std=c++11 xxx.cpp
编译并运行:
ydqun@ydqhost chapter02 % g++ -std=c++11 12.cpp [0]
ydqun@ydqhost chapter02 % ./a.out [0]
Found throw.
Throw is not blocked.
terminate called after throwing an instance of 'int'
[1] 963448 IOT instruction ./a.out
在上述代码中,我们可以看到出了BlockThrow()
调用了std::terminate
中断程序的执行,阻止了异常的继续传播外,Throw
和NoBlockThrow()
都抛出了异常,并且被捕获到。
3.noexcept作为运算符
而noexcept
作为一个操作符时,通常可以用于模板。比如:
template <class T>
void func() noexcept(noexcept(T())) {}
1.外层noexcept
:上文所介绍的noexcept
作为异常说明符的用法
外层的noexcept(...)
是函数的 异常说明符,用于声明func()
是否可能抛出异常:
- 语法:
noexcept
后跟一个布尔常量表达式,决定函数是否标记为noexcept
:- 若表达式为 true,则 func() 是 noexcept 的(不抛出异常)。
- 若为 false,则允许 func() 抛出异常。
2.内层noexcept
:运算符
内层的noexcept(T())
是noexcept
运算符,用于在编译期检查表达式T()
是否会抛出异常。
- 语法:
noexcept(expression)
- 若
expression
的求值 不会抛出异常,则返回true
。 - 若可能抛出异常,则返回
false
。
- 若
在此例中,noexcept(T())
检查类型T的默认构造函数(T())是否可能抛出异常。
3.组合逻辑
noexcept(noexcept(T()))
// ↑ ↑
// 说明符 运算符
- 整体含义:
func()
的noexcept
属性取决于T()
的异常行为。- 若
T()
是noexcept
的,则noexcept(T())
为true
, 所以func
也被标记为noexcept(true)
。 - 若
T()
可能抛出异常,则noexcept(T())
为false
,所以func()
的异常说明符为noexcept(false)
(即允许抛出异常)。
- 若
4.验证与调试
我们可以通过noexcept
运算符和static_assert
验证条件:
#include <iostream>
class A{
public:
A() noexcept {} //默认构造函数不抛出异常
};
class B {
public:
B() {} //默认构造函数可能跑出异常
};
template <class T>
void func() noexcept(noexcept(T())) {}
int main() {
static_assert(noexcept(A()), "A() should be noexcept.");//通过
static_assert(!noexcept(B()), "B() should not be noexcept.");//通过
static_assert(noexcept(func<A>()), "func<A> should be noexcept");//通过
static_assert(!noexcept(func<B>()), "func<B> should not be noexcept");//通过
return 0;
}
4.类析构函数默认是noexcept(true)
在C++11中,类的析构函数默认是隐式标记为noexcept(true)
的,但需要满足一下条件:
- 1.隐式生成的析构函数
- 如果用户未显示声明析构函数,且所有基类和非静态成员的析构函数均为
noexcept(true)
,则编译器生成的默认析构函数为noexcept(true)
。若任一基类或成员的析构函数为noexcept(false)
,则隐式生成的析构函数为noexcept(false)
。
- 如果用户未显示声明析构函数,且所有基类和非静态成员的析构函数均为
- 2.用户显示声明的析构函数
- 若用户声明函数但未指定
noexcept
,则默认标记为noexcept(true)
。例如:
- 若用户声明函数但未指定
~MyClass() {} // 默认为 noexcept(true)
此时,若析构函数抛出异常且为捕获,程序将调用std::terminate()
。
- 3.显示允许异常:
若通过noexcept(false)
显式声明析构函数,则允许析构函数抛出异常;否则,则析构函数默认是noexcept(true)
~MyClass() noexcept(false) { /* 可能抛出 */ }
- 我们可以看一下以下例子:
#include <iostream>
using namespace std;
struct A {
~A() { throw 1; }
};
struct B {
~B() noexcept(false) { throw 2; }
};
struct C {
B b;
};
void funA() { A a; }
void funB() { B b; }
void funC() { C c; }
int main() {
try {
funB();
}
catch(...) {
cout << "caught funB." << endl;
}
try {
funC();
}
catch(...) {
cout << "caught funC." << endl;
}
try {
funA(); //terminate called after throwing an istance of 'int'
}
catch (...) {
cout << "caught funA." << endl;
}
}
上述例子中,类A没有显示声明noexcept
,所以类A其细狗函数为默认noexcept(true)
,从而阻止了异常的扩散。而类B由于显式声明析构函数为noexcept(false)
,所以无论是类B还是包含了B类成员变量的类C,其析构函数都是可以抛出异常的。
总结:
默认情况下(未显式声明析构函数且基类/成员均无潜在抛出的析构函数),C++11中类的析构函数是noexcept(true)的。这一设计旨在避免栈展开时因析构函数抛异常导致程序终止,符合资源释放的安全实践。