提问人:Howard Hinnant 提问时间:8/5/2016 最后编辑:TemplateRexHoward Hinnant 更新时间:2/10/2020 访问量:3734
如何处理必须以异常安全方式获取多个资源的构造函数
How to handle constructors that must acquire multiple resources in an exception safe manner
问:
我有一个拥有多个资源的非平凡类型。如何以异常安全的方式构造它?
例如,下面是一个演示类,其中包含一个数组: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>
。这是一个很好的建议。但这只是必须拥有多个资源的更复杂场景的替代品,并且使用“使用 std::lib”的好建议并不方便。我选择用这种数据结构来传达这个问题,仅仅是因为它很熟悉。X
X
需要明确的是:如果你能设计出一个默认的正确清理所有内容(“零规则”),或者只需要释放一个资源,那么这是最好的。然而,在现实生活中,有时必须处理多种资源,这个问题解决了这些情况。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?!如果在编译时不知道资源数量怎么办?!
有没有更好的方法?
答:
有没有更好的方法?
是的
C++11 提供了一个名为委托构造函数的新功能,它非常优雅地处理这种情况。但这有点微妙。
在构造函数中抛出异常的问题在于,要意识到要构造的对象的析构函数在构造函数完成之前不会运行。尽管子对象(基和成员)的析构函数将在抛出异常时运行,但只要这些子对象被完全构造。
这里的关键是在开始向其添加资源之前完全构造,然后一次添加一个资源,在添加每个资源时保持有效状态。一旦完全构建,将在您添加资源时清理任何混乱。在 C++11 之前,这可能如下所示:X
X
X
~X()
X x; // no resources
x.push_back(A(1)); // add a resource
x.push_back(A(2)); // add a resource
// ...
但是在 C++11 中,您可以像这样编写多资源获取构造函数:
X(const A& x, const A& y)
: X{}
{
data_ = static_cast<A*>(::operator new (2*sizeof(A)));
::new(data_) A{x};
++size_;
::new(data_ + 1) A{y};
++size_;
}
这很像编写代码时完全不了解异常安全性。区别在于这一行:
: X{}
这说:为我构造一个默认的。在此构造之后,将完全构造,如果在后续操作中引发异常,则运行。这是革命性的!X
*this
~X()
请注意,在这种情况下,默认构造不会获取任何资源。事实上,它甚至是含蓄的.所以那部分不会扔掉。它设置为保存大小为 0 的数组的有效值。 知道如何应对这种状态。X
noexcept
*this
X
~X()
现在添加未初始化内存的资源。如果抛出,您仍然会构建一个默认值,并通过不执行任何操作来正确处理它。X
~X()
现在添加第二个资源:构造的副本。如果抛出,仍将释放缓冲区,但不运行任何 .x
~X()
data_
~A()
如果第二个资源成功,则通过递增操作将 设置为有效状态。如果在此之后抛出任何内容,将正确清理长度为 1 的缓冲区。X
size_
noexcept
~X()
现在尝试第三个资源:构造的副本。如果抛出该构造,将正确清理长度为 1 的缓冲区。如果它没有抛出,则通知它现在拥有长度为 2 的缓冲区。y
~X()
*this
使用此技术不需要默认可构造。例如,默认构造函数可以是私有的。或者,您可以使用其他一些私有构造函数,将其置于无资源状态:X
X
: X{moved_from_tag{}}
在 C++11 中,如果你可以有一个无资源的状态,这通常是一个好主意,因为这使您能够拥有一个与各种优点捆绑在一起的移动构造函数(并且是另一篇文章的主题)。X
noexcept
C++11 委托构造函数是一种非常好的(可扩展)技术,用于编写异常安全构造函数,只要您在开始时有一个无资源状态(例如,noexcept 默认构造函数)。
是的,在 C++98/03 中有一些方法可以做到这一点,但它们并不那么漂亮。您必须创建一个 的 implementation-detail 基类,该基类包含 的销毁逻辑,但不包含构造逻辑。去过那里,做过,我喜欢委派构造函数。X
X
评论
std::vector<A>
data_.reserve(2)
data_.push_back(...)
++size
在 C++11 中,也许可以尝试这样的东西:
#include "A.h"
#include <vector>
class X
{
std::vector<A> data_;
public:
X() = default;
X(const A& x, const A& y)
: data_{x, y}
{
}
// ...
};
我认为问题源于对单一责任原则的违反:X 类必须处理管理多个对象的生命周期(这可能甚至不是它的主要责任)。
类的析构函数应仅释放该类直接获取的资源。如果类只是一个复合类(即该类的实例拥有其他类的实例),理想情况下,它应该依赖于自动内存管理(通过 RAII),并且只使用默认的析构函数。如果类必须手动管理一些专用资源(例如,打开文件描述符或连接、获取锁或分配内存),我建议将管理这些资源的责任分解为专用于此目的的类,然后使用该类的实例作为成员。
使用标准模板库实际上会有所帮助,因为它包含专门处理此问题的数据结构(例如智能指针和 )。它们也可以组合,因此即使您的 X 必须包含具有复杂资源获取策略的多个对象实例,也可以为每个成员以及包含复合类 X 解决以异常安全方式进行资源管理的问题。std::vector<T>
下一个:C++17 有哪些新功能?
评论
new/delete
new/delete
std::unique_ptr
X