目录
[1. 直接结束程序(abort)](#1. 直接结束程序(abort))
[2. 吞下异常(记录日志)](#2. 吞下异常(记录日志))
[3. 让客户自己解决(推荐)](#3. 让客户自己解决(推荐))
一、数组析构时的异常问题
假设 vector<Widget> 中的 Widget 对象在析构时抛出异常。
C++ 规定:同时存在两个未处理的异常时,程序会终止。
由于第一个 Widget 析构抛出异常时,第二个、第三个 Widget 的析构还在继续,此时就有两个异常同时存在,因此程序会被终止。
此外,析构函数抛出异常可能导致内存释放不彻底,造成资源泄漏。
结论:析构函数最好不要抛出异常。
C++ 新标准 :规定 vector 等容器在析构时不能抛出异常,否则程序直接终止。
解决方案 :使用 noexcept 关键字,声明该函数不会抛出异常。如果实际抛出了异常,会直接调用 std::terminate() 结束程序。
二、用对象管理资源时的析构异常
有时会用一个额外的 manageA 对象来管理对象 A,当 manageA 生命周期结束时,其析构函数会调用 A 的 close 接口。
cpp
class A {
public:
static A create() {
return A();
}
void close() {} // 没人规定 close 不会抛异常
};
class manageA {
public:
manageA() {
_a = A::create();
}
~manageA() {
_a.close(); // 如果 close 抛异常,析构函数就会抛出异常
}
private:
A _a;
};
如果 close 接口可能抛出异常,那么 manageA 的析构函数就可能抛出异常,这很麻烦。
三、处理析构异常的三种策略
1. 直接结束程序(abort)
最粗暴的方法:在析构函数中捕获异常,然后调用 abort() 结束程序。
cpp
class manageA {
public:
manageA() {
_a = A::create();
}
~manageA() {
try {
_a.close();
} catch (...) {
abort(); // 捕获任何异常后立即终止程序
}
}
private:
A _a;
};
合理性:阻止异常从析构函数中传播出去,符合 C++ 的规范要求。
2. 吞下异常(记录日志)
捕获异常但不传播,仅记录日志。
cpp
~manageA() {
try {
_a.close();
} catch (...) {
LOG(LogLevel::DEBUG) << "error"; // 记录错误日志
}
}
要求:采用这种方案,必须确保吞掉异常后,程序仍然能够正常运行。
有时候吞下异常的方式比 abort 结束程序更好。
3. 让客户自己解决(推荐)
最好的方式是将问题甩给程序员,而不是预设是吞掉还是结束。
cpp
class manageA {
public:
manageA() {
_a = A::create();
}
// 提供 close 接口,让用户主动调用
void close() {
_a.close();
}
~manageA() {
try {
_a.close();
} catch (...) {
// 什么都不做,或者记录日志
// 但此时异常已经由用户主动调用 close 时处理过了
}
}
private:
A _a;
};
设计思路:
-
提供一个
close接口,让用户有机会主动调用 -
如果用户调用了
close接口,就有充足的时间去处理可能抛出的异常 -
如果用户没有调用
close接口,那么析构时出现异常,责任在用户------因为他本来有机会去调用close接口
四、关于"甩锅"的讨论
作者承认:这个行为看起来像是在甩锅给用户,可能有人认为这违反了"让接口容易正确使用"的准则。
反驳:
-
提供一个
close接口,反而是给了用户处理异常的机会 -
用户可以选择调用
close来主动处理异常,也可以选择不调用 -
不调用的后果由用户自己承担
这是一种责任分离的设计:资源管理类负责自动释放资源,但异常处理的责任交给用户主动调用接口来完成。