How*_*ant 32 c++ exception-handling c++-faq c++11 delegating-constructor
我有一个拥有多种资源的非平凡类型.如何以异常安全的方式构建它?
例如,这是一个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;
// ...
};
Run Code Online (Sandbox Code Playgroud)
现在这个特定类的明显答案就是使用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};
}
Run Code Online (Sandbox Code Playgroud)
我有一个完全检测的测试类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";
}
}
Run Code Online (Sandbox Code Playgroud)
输出是:
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)
Run Code Online (Sandbox Code Playgroud)
我有4个构造和4个析构,每个销毁都有一个匹配的构造函数.一切都很好.
但是如果复制构造函数A{2}
抛出异常,我得到这个输出:
A(int state): 1
A(int state): 2
Begin
A(A const& a): 1
Exceptional End
~A(2)
~A(1)
Run Code Online (Sandbox Code Playgroud)
现在我有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;
}
}
Run Code Online (Sandbox Code Playgroud)
这正确输出:
A(int state): 1
A(int state): 2
Begin
A(A const& a): 1
~A(1)
Exceptional End
~A(2)
~A(1)
Run Code Online (Sandbox Code Playgroud)
但这太丑了! 如果有4个资源怎么办?还是400?! 如果在编译时不知道资源的数量怎么办?!
有更好的方法吗?
How*_*ant 37
有更好的方法吗?
是
C++ 11提供了一个名为委托构造函数的新功能,可以非常优雅地处理这种情况.但它有点微妙.
在构造函数中抛出异常的问题是要意识到你构造的对象的析构函数在构造函数完成之前不会运行.虽然子对象(基础和成员)的析构函数将在抛出异常时运行,只要这些子对象完全构造即可.
这里的关键是要全面建设X
之前你开始增加资源给它,然后添加资源一次一个,保持X
有效状态,你添加的每个资源.一旦X
完全构建,~X()
将在添加资源时清理任何混乱.在C++ 11之前,这可能看起来像:
X x; // no resources
x.push_back(A(1)); // add a resource
x.push_back(A(2)); // add a resource
// ...
Run Code Online (Sandbox Code Playgroud)
但是在C++ 11中,您可以像这样编写multi-resource-acquizition构造函数:
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_;
}
Run Code Online (Sandbox Code Playgroud)
这非常类似于编写完全不了解异常安全的代码.区别在于这一行:
: X{}
Run Code Online (Sandbox Code Playgroud)
这说:构造一个默认值X
.在构造之后,*this
完全构造并且如果在后续操作中抛出异常,则~X()
运行. 这是革命性的!
请注意,在这种情况下,default-construct X
不会获取资源.实际上,它甚至是隐含的noexcept
.所以这部分不会抛出.它设置*this
为一个有效的X
,它包含一个大小为0的数组. ~X()
知道如何处理该状态.
现在添加未初始化内存的资源.如果抛出,你仍然有一个默认构造,X
并~X()
通过什么都不做正确处理.
现在添加第二个资源:构建的副本x
.如果抛出,~X()
仍将释放data_
缓冲区,但不运行任何缓冲区~A()
.
如果第二资源成功,将X
通过增加一个有效的状态,size_
这是一种noexcept
操作.如果抛出此后的任何内容,~X()
将正确清理长度为1的缓冲区.
现在尝试第三种资源:构建的副本y
.如果该构造抛出,~X()
将正确清理长度为1的缓冲区.如果它没有抛出,请告知*this
它现在拥有一个长度为2的缓冲区.
该技术的使用并不需要X
为缺省构造.例如,默认构造函数可以是私有的.或者您可以使用其他一些X
进入无资源状态的私有构造函数:
: X{moved_from_tag{}}
Run Code Online (Sandbox Code Playgroud)
在C++ 11中,如果你X
可以拥有一个无资源状态通常是个好主意,因为这样你就可以拥有一个noexcept
捆绑了各种优点的移动构造函数(并且是不同帖子的主题).
C++ 11委托构造函数是一种非常好的(可伸缩的)技术,用于编写异常安全构造函数,只要您在开始时具有无资源状态(例如,noexcept默认构造函数).
是的,有很多方法可以在C++ 98/03中实现,但它们并不漂亮.您必须创建一个X
包含销毁逻辑X
但不包含构造逻辑的实现细节基类.去过那里,做到了,我喜欢委托施工人员.
我认为问题源于违反单一责任原则:X类必须处理管理多个对象的生命周期(这可能不是它的主要责任).
类的析构函数应该只释放类直接获取的资源.如果该类只是一个复合(即该类的实例拥有其他类的实例),理想情况下它应该依赖于自动内存管理(通过RAII)并且只使用默认的析构函数.如果类必须手动管理一些专用资源(例如打开文件描述符或连接,获取锁或分配内存),我建议将管理这些资源的责任分解为专用于此目的的类,然后使用那个班级成员.
使用标准模板库实际上会有所帮助,因为它包含std::vector<T>
专门处理此问题的数据结构(如智能指针和).它们也是可复制的,因此即使您的X必须包含具有复杂资源获取策略的多个对象实例,也会针对每个成员以及包含复合类X来解决异常安全方式的资源管理问题.