C++的异常规范用于声明函数可能抛出的异常类型,随着标准迭代,先后出现了throw()和noexcept两种主流的异常说明方式,两者在语义、编译器处理、运行时行为上都有明显区别,了解这些差异对编写健壮的C++代码很有帮助。

throw()的基本用法与特性
throw()是C++98标准中引入的异常规范语法,用于声明函数不会抛出任何异常,或者指定函数可能抛出的异常类型。如果声明为不抛异常的函数实际抛出了异常,程序会调用std::unexpected函数,默认情况下会调用std::terminate终止程序。
声明不抛异常的函数的语法如下:
// 声明func函数不会抛出任何异常
void func() throw() {
// 函数逻辑
}
// 声明bar函数可能抛出int或const char*类型的异常
void bar() throw(int, const char*) {
// 函数逻辑
}
throw()的局限性比较明显:首先它只是运行时的检查,编译器不会在编译阶段对异常抛出的行为做严格校验;其次如果违反规范,触发的是std::unexpected流程,行为不够直观,而且会带来一定的运行时开销。
noexcept的基本用法与特性
noexcept是C++11标准引入的异常说明符,用于替代throw()的不抛异常声明,它的语义更明确,编译器可以做更多优化。noexcept有两种常见用法:不带参数的noexcept表示函数不会抛出任何异常,带布尔常量表达式的noexcept可以根据表达式结果决定函数是否可能抛异常。
基本语法示例如下:
// 声明func函数不会抛出任何异常
void func() noexcept {
// 函数逻辑
}
// 根据表达式结果决定异常说明,这里表示func2不会抛异常
void func2() noexcept(true) {
// 函数逻辑
}
// 声明bar函数可能抛出异常
void bar() noexcept(false) {
throw 1;
}
如果标记为noexcept的函数实际抛出了异常,程序会直接调用std::terminate终止,不会走额外的std::unexpected流程,行为更明确。同时编译器可以在编译阶段对noexcept函数做更多优化,比如移动构造函数的优化、容器扩容时的逻辑优化等。
noexcept和throw()的核心区别
两者的主要差异可以从以下几个方面对比:
| 对比维度 | throw() | noexcept |
|---|---|---|
| 所属标准 | C++98及之前 | C++11及之后 |
| 不抛异常语义 | 运行时检查,违反时调用std::unexpected | 编译期+运行时语义,违反时直接调用std::terminate |
| 编译器优化支持 | 几乎不支持优化,有运行时开销 | 支持编译器优化,无额外运行时开销 |
| 可组合性 | 不支持表达式判断 | 支持布尔常量表达式,可配合模板使用 |
| 类型系统支持 | 不属于函数类型的一部分 | 属于函数类型的一部分,函数指针可以携带noexcept信息 |
下面是一个违反异常规范的对比示例:
#include <iostream>
#include <exception>
// throw()声明的函数抛出异常的旧行为
void old_func() throw() {
throw 1; // 违反throw(),会调用std::unexpected,默认终止
}
// noexcept声明的函数抛出异常的新行为
void new_func() noexcept {
throw 1; // 违反noexcept,直接调用std::terminate终止
}
int main() {
try {
old_func();
} catch(...) {
std::cout << "catch old func exception" << std::endl;
}
// 实际运行old_func时不会走到catch,因为已经触发终止
return 0;
}
C++异常规范的演进过程
C++98/03阶段:throw()异常规范
早期C++引入throw(类型列表)的语法,目的是让开发者明确函数抛出的异常类型,方便调用方处理。但实际使用中暴露了很多问题:首先编译器无法在编译期校验异常抛出是否符合声明,只能在运行时处理;其次如果函数抛出了声明之外的异常,流程复杂,而且动态异常规范会带来额外的运行时开销,很多编译器甚至直接忽略throw()的优化作用。
C++11阶段:noexcept替代throw()
C++11意识到throw()的设计缺陷,引入了noexcept关键字,同时废弃了throw(类型列表)的动态异常规范语法(虽然仍保留支持,但不推荐使用)。noexcept的设计更贴合实际需求:不抛异常的声明是编译期可感知的,编译器可以据此做优化,而且违反规范的行为更直接,减少不必要的复杂度。同时C++11还引入了noexcept运算符,可以在编译期判断一个表达式是否会抛出异常,方便模板代码编写。
noexcept运算符的使用示例:
#include <iostream>
#include <utility>
struct MyStruct {
MyStruct(MyStruct&&) noexcept { // 移动构造声明为noexcept
}
};
int main() {
// 判断移动构造是否noexcept,结果为true
std::cout << noexcept(MyStruct(std::declval<MyStruct>())) << std::endl;
return 0;
}
C++17及之后:进一步完善noexcept相关规则
C++17进一步调整了异常规范的相关规则,比如默认情况下,一些特殊成员函数(如移动构造、移动赋值)的noexcept属性会根据其成员的类型自动推导,不需要开发者手动声明。同时标准库中也大量使用noexcept来标记不抛异常的函数,提升标准库的性能。
实际使用建议
在新项目中,优先使用noexcept来声明不抛异常的函数,尤其是移动构造、移动赋值、析构函数、 swap函数等,这些函数标记为noexcept可以让标准容器(如vector、map)在扩容、重新分配内存时优先使用移动操作而不是拷贝操作,提升性能。
对于旧的代码中使用throw()的地方,如果没有兼容C++11之前版本的需求,建议逐步替换为noexcept。如果不确定函数是否会抛异常,不要随意标记noexcept,否则一旦函数内部抛出异常,程序会直接终止,带来更严重的问题。
注意:析构函数默认是noexcept的,除非析构函数内部显式抛出了异常,或者类的某个非静态成员/基类的析构函数不是noexcept的,才会改变析构函数的noexcept属性。