我有一个拥有多种资源的重要类型。如何以异常安全的方式构造它?
例如,这里有一个演示类X
,它包含一个数组A
:
#include "A.h"
class X
{
unsigned size_ = 0;
A* data_ = nullptr;
public:
~X()
{
for (auto p = data_; p < data_ + size_; ++p)
p->~A();
::operator delete(data_);
}
X() = default;
// ...
};
现在这个特定类的明显答案是使用std::vector<A>
. 这是个好建议。但这X
只是更复杂的场景的替代品,其中X
必须拥有多个资源,并且使用“使用 std::lib”的好建议并不方便。我选择用这个数据结构来交流这个问题仅仅是因为它很熟悉。
非常清楚:如果您可以设计您X
的默认值以~X()
正确清理所有内容(“零规则”),或者~X()
只需要释放单个资源,那么这是最好的。但是,在现实生活中有时~X()
必须处理多种资源,而这个问题解决了这些情况。
所以这个类型已经有了一个很好的析构函数和一个很好的默认构造函数。我的问题集中在一个重要的构造函数上,它需要两个A
,为它们分配空间并构造它们:
X::X(const A& x, const A& y)
: size_{2}
, data_{static_cast<A*>(::operator new (size_*sizeof(A)))}
{
::new(data_) A{x};
::new(data_ + 1) A{y};
}
我有一个完全仪器化的测试类A
,如果这个构造函数没有抛出异常,它工作得很好。例如,使用此测试驱动程序:
int
main()
{
A a1{1}, a2{2};
try
{
std::cout << "Begin\n";
X x{a1, a2};
std::cout << "End\n";
}
catch (...)
{
std::cout << "Exceptional End\n";
}
}
输出是:
A(int state): 1
A(int state): 2
Begin
A(A const& a): 1
A(A const& a): 2
End
~A(1)
~A(2)
~A(2)
~A(1)
我有 4 个构造和 4 个破坏,每个破坏都有一个匹配的构造函数。一切都很好。
但是,如果 的复制构造函数A{2}
抛出异常,我会得到以下输出:
A(int state): 1
A(int state): 2
Begin
A(A const& a): 1
Exceptional End
~A(2)
~A(1)
现在我有 3 个结构,但只有 2 个破坏。结果泄露A
了!A(A const& a): 1
解决此问题的一种方法是将构造函数与try/catch
. 然而,这种方法是不可扩展的。在每次分配资源之后,我需要另一个嵌套try/catch
来测试下一个资源分配并取消分配已经分配的内容。握住鼻子:
X(const A& x, const A& y)
: size_{2}
, data_{static_cast<A*>(::operator new (size_*sizeof(A)))}
{
try
{
::new(data_) A{x};
try
{
::new(data_ + 1) A{y};
}
catch (...)
{
data_->~A();
throw;
}
}
catch (...)
{
::operator delete(data_);
throw;
}
}
这正确输出:
A(int state): 1
A(int state): 2
Begin
A(A const& a): 1
~A(1)
Exceptional End
~A(2)
~A(1)
但这很丑! 如果有 4 个资源呢?还是400?! 如果在编译时不知道资源数量怎么办?!
有没有更好的方法?