noexcept是C++11引入的关键字,用于指定函数是否可能抛出异常,是现代C++异常规范的核心组成部分,替代了早期C++版本中动态异常规范的老旧写法,为代码的安全性和性能优化提供了明确的语义支持。

noexcept的基本语法
noexcept有两种常见的使用形式,一种是无条件的noexcept指定,另一种是带条件表达式的noexcept指定,具体语法如下:
// 无条件指定函数不会抛出异常
void func1() noexcept {
// 函数逻辑
}
// 带条件表达式的noexcept指定,表达式结果为true时函数不抛出异常
void func2() noexcept(noexcept(表达式)) {
// 函数逻辑
}
// 判断表达式是否会抛出异常,返回bool常量
noexcept(表达式)
如果函数在声明时标记了noexcept,那么函数内部如果抛出了异常,程序会直接调用std::terminate终止运行,不会按照正常的异常捕获流程处理。
noexcept的核心作用
明确异常语义,提升代码可读性
在没有noexcept之前,开发者只能通过注释说明函数是否会抛出异常,而noexcept将这种约定变成了语法层面的约束。当其他开发者看到函数标记了noexcept,就可以明确知道调用这个函数不需要做异常捕获处理,减少了沟通成本和误用概率。
助力编译器优化
编译器在知道函数不会抛出异常的情况下,可以省略很多用于异常处理的额外代码,比如栈展开的相关逻辑,从而生成更高效的机器码。尤其是在循环调用标记了noexcept的小函数时,优化效果会更明显。
影响标准库的行为
标准库中的很多组件会检查相关操作是否标记了noexcept,其中最典型的是移动构造和移动赋值函数。如果类的移动构造和移动赋值标记了noexcept,标准库容器在进行扩容、重新分配内存等操作时,会优先使用移动语义而不是拷贝语义,大幅提升性能。
例如vector在扩容时,需要将原有元素转移到新的内存空间中,如果元素的移动构造是noexcept的,vector会使用移动操作,否则会使用拷贝操作,因为移动操作如果抛出异常,会导致原有元素丢失,而拷贝操作即使抛出异常也不会破坏原有数据。
noexcept的实践建议
适合标记noexcept的场景
- 析构函数:几乎所有析构函数都不应该抛出异常,默认情况下析构函数都是隐式noexcept的,不需要手动标记,除非你在析构函数中做了可能抛异常的操作。
- 移动构造函数和移动赋值函数:如果移动操作不会抛出异常,一定要标记noexcept,否则标准库容器无法使用移动语义优化。
- 简单的工具函数:比如获取成员变量的值、简单的计算函数,这些函数逻辑简单不会抛异常,适合标记noexcept。
- 明确不会抛异常的函数:如果函数内部没有调用任何可能抛异常的操作,并且逻辑上也不应该抛异常,就可以标记noexcept。
不适合标记noexcept的场景
- 可能调用抛异常接口的函数:如果函数内部调用了可能抛异常的函数,并且没有捕获这些异常,就不要标记noexcept,否则异常抛出会直接导致程序终止。
- 虚函数重写:如果基类的虚函数没有标记noexcept,那么派生类重写这个虚函数时也不要随意添加noexcept,否则会导致函数签名不匹配,无法实现多态。
代码示例
下面是一个完整的示例,展示noexcept在移动语义场景下的作用:
#include <iostream>
#include <vector>
#include <utility>
class MyString {
private:
char* data;
size_t len;
public:
// 构造函数
MyString(const char* str = nullptr) {
if (str) {
len = strlen(str);
data = new char[len + 1];
strcpy(data, str);
} else {
len = 0;
data = new char[1];
data[0] = ' ';
}
}
// 拷贝构造函数
MyString(const MyString& other) {
len = other.len;
data = new char[len + 1];
strcpy(data, other.data);
std::cout << "拷贝构造被调用" << std::endl;
}
// 移动构造函数,标记noexcept
MyString(MyString&& other) noexcept {
len = other.len;
data = other.data;
other.data = nullptr;
other.len = 0;
std::cout << "移动构造被调用" << std::endl;
}
// 析构函数
~MyString() {
delete[] data;
}
// 获取字符串内容
const char* c_str() const noexcept {
return data ? data : "";
}
};
int main() {
std::vector<MyString> vec;
// 预留空间,避免首次扩容
vec.reserve(10);
vec.push_back(MyString("hello"));
vec.push_back(MyString("world"));
// 此时vector扩容,会调用移动构造
vec.push_back(MyString("test"));
for (const auto& s : vec) {
std::cout << s.c_str() << std::endl;
}
return 0;
}
如果把移动构造函数的noexcept去掉,那么在vector扩容时就会调用拷贝构造函数,大家可以自行修改代码测试效果。
注意事项
不要随意给函数标记noexcept,一定要确认函数确实不会抛出异常再标记。如果函数标记了noexcept但内部还是抛出了异常,程序会直接终止,这种错误很难排查。另外,noexcept是函数接口的一部分,如果修改了函数的noexcept属性,可能需要重新编译所有依赖这个函数的代码,所以在设计接口时要谨慎考虑noexcept的标记。
总的来说,noexcept是现代C++中非常重要的一个关键字,合理使用它可以让代码更清晰、更高效,也能更好地适配标准库的行为,是每一位C++开发者都应该掌握的特性。