noexcept 与异常安全
定义与作用
C++11 引入 noexcept 替代弃用的动态异常规范(throw())。noexcept 有两个角色:
- noexcept 说明符:声明函数不抛异常
- noexcept 运算符:编译期检测表达式是否会抛异常
void safe_func() noexcept; // 声明不抛异常
bool b = noexcept(safe_func()); // 编译期检测:true
异常安全(Exception Safety)是 C++ 程序健壮性的核心保证——即使在异常发生时,程序状态仍可预期。
核心原理
noexcept 说明符与移动语义的关系
三级异常安全保证
完整示例
示例一:noexcept 对性能的影响
场景说明:对比移动构造加不加 noexcept 对 std::vector 扩容行为的影响。
#include <iostream>
#include <vector>
#include <chrono>
#include <string>
// 不带 noexcept 的移动构造
struct NoNoexcept {
std::string data;
NoNoexcept(std::string s) : data(std::move(s)) {
std::cout << " 构造: " << data << std::endl;
}
NoNoexcept(const NoNoexcept& other) : data(other.data) {
std::cout << " 拷贝: " << data << std::endl;
}
NoNoexcept(NoNoexcept&& other) // 故意不加 noexcept
: data(std::move(other.data)) {
std::cout << " 移动: " << data << std::endl;
}
};
// 带 noexcept 的移动构造
struct YesNoexcept {
std::string data;
YesNoexcept(std::string s) : data(std::move(s)) {
std::cout << " 构造: " << data << std::endl;
}
YesNoexcept(const YesNoexcept& other) : data(other.data) {
std::cout << " 拷贝: " << data << std::endl;
}
YesNoexcept(YesNoexcept&& other) noexcept // 加了 noexcept
: data(std::move(other.data)) {
std::cout << " 移动: " << data << std::endl;
}
};
// ====== noexcept 运算符的编译期检测 ======
void may_throw() {} // 可能抛异常
void never_throws() noexcept {} // 绝不抛异常
template <typename T>
void check_noexcept() {
std::cout << typeid(T).name() << " 移动构造 noexcept? "
<< std::boolalpha
<< noexcept(T(std::declval<T&&>())) // noexcept 运算符
<< std::endl;
}
int main() {
std::cout << "===== noexcept 运算符 =====" << std::endl;
std::cout << "may_throw() noexcept? " << noexcept(may_throw()) << std::endl;
std::cout << "never_throws() noexcept? " << noexcept(never_throws()) << std::endl;
std::cout << "1 + 1 noexcept? " << noexcept(1 + 1) << std::endl;
std::cout << "\n===== 类型检测 =====" << std::endl;
check_noexcept<NoNoexcept>();
check_noexcept<YesNoexcept>();
check_noexcept<std::string>();
check_noexcept<int>();
std::cout << "\n===== vector 扩容:无 noexcept =====" << std::endl;
{
std::vector<NoNoexcept> vec;
vec.emplace_back("A");
std::cout << "--- 添加第二个元素(触发扩容)---" << std::endl;
vec.emplace_back("B"); // 扩容时使用拷贝而非移动!
}
std::cout << "\n===== vector 扩容:有 noexcept =====" << std::endl;
{
std::vector<YesNoexcept> vec;
vec.emplace_back("X");
std::cout << "--- 添加第二个元素(触发扩容)---" << std::endl;
vec.emplace_back("Y"); // 扩容时使用移动!
}
return 0;
}
预期输出:
===== noexcept 运算符 =====
may_throw() noexcept? false
never_throws() noexcept? true
1 + 1 noexcept? true
===== 类型检测 =====
...
===== vector 扩容:无 noexcept =====
构造: A
--- 添加第二个元素(触发扩容)---
构造: B
拷贝: A ← 用拷贝而不用移动!
===== vector 扩容:有 noexcept =====
构造: X
--- 添加第二个元素(触发扩容)---
构造: Y
移动: X ← 用移动!
逐段分析:
noexcept(表达式)在编译期返回 bool——表达式是否会抛异常- 不带
noexcept的移动构造 → vector 扩容回退到拷贝——拷贝安全但慢 - 带
noexcept的移动构造 → vector 扩容使用移动——移动高效 int的移动天生 noexcept——所有基本类型的移动操作都 noexcept
示例二:异常安全的银行转账
#include <iostream>
#include <string>
#include <stdexcept>
#include <map>
class BankAccount {
public:
BankAccount(std::string owner, double balance)
: owner_(std::move(owner)), balance_(balance) {}
// 转账——强异常安全保证
void transfer(BankAccount& to, double amount) {
if (this == &to) return;
// 必要检查(不涉及状态修改)
if (amount <= 0)
throw std::invalid_argument("转账金额必须为正数");
if (balance_ < amount)
throw std::runtime_error(owner_ + " 余额不足");
// 先修改对方账户(可能抛异常)
to.balance_ += amount;
// 再修改自己账户(不抛异常)
balance_ -= amount;
std::cout << " 转账: " << owner_ << " → " << to.owner_
<< " ¥" << amount << std::endl;
}
void deposit(double amount) {
if (amount <= 0)
throw std::invalid_argument("存款金额必须为正数");
balance_ += amount;
}
void withdraw(double amount) {
if (amount <= 0)
throw std::invalid_argument("取款金额必须为正数");
if (balance_ < amount)
throw std::runtime_error("余额不足");
balance_ -= amount;
}
void print() const {
std::cout << " " << owner_ << ": ¥" << balance_ << std::endl;
}
// 获取余额——noexcept 保证
double balance() const noexcept { return balance_; }
// swap 操作——noexcept
void swap(BankAccount& other) noexcept {
std::swap(owner_, other.owner_);
std::swap(balance_, other.balance_);
}
private:
std::string owner_;
double balance_;
};
int main() {
std::cout << "===== 正常转账 =====" << std::endl;
BankAccount alice("张三", 5000);
BankAccount bob("李四", 2000);
alice.print();
bob.print();
try {
alice.transfer(bob, 1000);
std::cout << "转账成功" << std::endl;
} catch (const std::exception& e) {
std::cerr << "转账失败: " << e.what() << std::endl;
}
alice.print();
bob.print();
std::cout << "\n===== 异常场景 =====" << std::endl;
// 场景 1:余额不足
try {
bob.transfer(alice, 100000);
} catch (const std::runtime_error& e) {
std::cerr << "失败: " << e.what() << std::endl;
}
// 场景 2:负数转账
try {
alice.transfer(bob, -100);
} catch (const std::invalid_argument& e) {
std::cerr << "失败: " << e.what() << std::endl;
}
std::cout << "\n最终状态:" << std::endl;
alice.print();
bob.print();
std::cout << "李四余额 noexcept? true" << std::endl;
return 0;
}
预期输出:
===== 正常转账 =====
张三: ¥5000
李四: ¥2000
转账: 张三 → 李四 ¥1000
转账成功
张三: ¥4000
李四: ¥3000
===== 异常场景 =====
失败: 李四 余额不足
失败: 转账金额必须为正数
最终状态:
张三: ¥4000
李四: ¥3000
李四余额 noexcept? true
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 移动构造/赋值不加 noexcept | STL 容器回退到拷贝 | 所有移动操作标记 noexcept |
| noexcept 函数中抛异常 | 直接 terminate | 确保 noexcept 函数内 catch 所有异常 |
| 修改状态后再抛异常 | 不满足强异常保证 | 先做不修改状态的检查,再改对方,最后改自己 |
| swap 不是 noexcept | copy-and-swap 不安全 | swap 必须 noexcept |
常见面试问题
为什么移动构造应该标记 noexcept?——STL 容器(如
std::vector)在扩容等操作时,如果元素移动构造不是 noexcept,会回退到拷贝构造以保证异常安全。标记 noexcept 让容器放心使用更高效的移动语义。三级异常安全保证分别是什么?——(1) 不抛出保证(noexcept:绝无异常);(2) 强保证(操作成功或状态不变,如 copy-and-swap);(3) 基本保证(不泄漏资源,对象处于有效但可能改变的状态)。
noexcept 运算符的用途?——编译期检测表达式是否会抛异常。常用于模板代码:
noexcept(T(std::declval<T&&>()))检测移动构造是否 noexcept。noexcept(true) 和 noexcept(false) 的区别?——
noexcept等价于noexcept(true),声明不抛异常。noexcept(false)等价于不写。noexcept 接受编译期 bool 表达式:noexcept(cond)。析构函数为什么隐式 noexcept?——C++11 起析构函数默认 noexcept,因为栈展开期间析构函数抛异常会导致
std::terminate。绝对不要在析构函数中抛异常。
小结
noexcept说明符声明函数不抛异常——违反则 terminatenoexcept运算符编译期检测表达式异常性——模板元编程利器- 移动构造/赋值必须标记
noexcept——直接影响 STL 容器的性能策略 - 三级异常安全保证:noexcept → 强保证 → 基本保证
- 析构函数隐式 noexcept——RAII 异常安全的最后防线