构造函数抛出异常需避免相关资源泄漏
2.11 ID_throwInConstructor
如果构造函数抛出异常,对应的析构函数将不会被执行,因此应保证已分配的资源能够被有效回收。
示例:
class A {
int *a, *b;
public:
A(size_t n):
a(new int[n]),
b(new int[n]) // The allocations may fail
{
if (sth_wrong) {
throw E(); // User exceptions
}
}
~A() { // May be invalid
delete[] a;
delete[] b;
}
};
例中内存分配可能会失败,抛出 bad_alloc 异常,在某种条件下还会抛出自定义的异常,任何一种异常被抛出析构函数就不会被执行,已分配的资源就无法被回收,但已构造完毕的基类对象和成员对象还是会正常析构的,所以应采用对象化资源管理方法,使资源可以被自动回收,可参见 ID_ownerlessResource 的进一步讨论。
可改为:
A::A(size_t n) {
// Use objects to hold resources
auto holder_a = make_unique<int[]>(n);
auto holder_b = make_unique<int[]>(n);
// Do the tasks that may throw exceptions
if (sth_wrong) {
throw E();
}
// Transfer ownership, make sure no exception is thrown
a = holder_a.release();
b = holder_b.release();
}
先用 unique_ptr 对象持有资源,完成可能抛出异常的事务之后,再将资源转移给相关成员,转移的过程不可抛出异常,这种模式可以保证异常安全,如果有异常抛出,资源均可被正常回收。对遵循 C++11 及之后标准的代码,建议用 make_unique 等工厂函数代替 new 运算符。
示例代码意在讨论一种通用模式,实际代码可采用更直接的方式:
class A {
vector<int> a, b; // Or use ‘unique_ptr’
public:
A(size_t n): a(n), b(n) { // Safe and brief
....
}
};
保证已分配的资源时刻有对象负责回收是重要的设计原则。
注意,“未成功初始化的对象”在 C++ 语言中是不存在的,应避免相关逻辑错误,如:
struct T {
A() { throw CtorException(); }
};
void foo() {
T* p = nullptr;
try {
p = new T;
}
catch (CtorException&) {
delete p; // Logic error, ‘p’ is nullptr
return;
}
....
delete p;
}
例中 T 类型的对象在构造时抛出异常,而实际上 p 并不会指向一个未能成功初始化的对象,赋值被异常中断,catch 中的 p 仍然是一个空指针,new 表达式中抛出异常会自动回收已分配的内存。
相关
依据
ISO/IEC 14882:2003 5.3.4(17)
ISO/IEC 14882:2011 5.3.4(18)
ISO/IEC 14882:2017 8.3.4(21)