如何处理必须以异常安全方式获取多个资源的构造函数

时间:2016-08-05 03:26:02

标签: c++ c++11 exception-handling c++-faq 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;
    // ...
};

现在,这个特定类的明显答案是使用std::vector<A> 。这是一个很好的建议。但X只是X必须拥有多个资源的更复杂场景的替身,并且使用&#34;使用std :: lib的好建议并不方便&#34;我之所以选择用这个数据结构来传达这个问题只是因为它很熟悉。

明确 crystal :如果你可以设计你的X,以便默认~X()正确清理所有内容(&#34;规则为零&#34; ),或者如果~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?!如果在编译时不知道资源的数量怎么办?

是否有更好的方式?

3 个答案:

答案 0 :(得分:35)

  

是否有更好的方式?

C ++ 11提供了一个名为委托构造函数的新功能,可以优雅地处理这种情况 very 。但它有点微妙。

在构造函数中抛出异常的问题是要意识到你构造的对象的析构函数在构造函数完成之前不会运行。尽管如果抛出异常,子对象(基础和成员)的析构函数将会运行,只要这些子对象完全构造完毕。

这里的关键是在开始向其添加资源之前完全构建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
// ...

但是在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()将运行。 这是革命性的!

请注意,在这种情况下,默认构造的X不会获取任何资源。实际上,它甚至含蓄地noexcept。所以这部分不会抛出。它将*this设置为包含大小为0的数组的有效X~X()知道如何处理该状态。

现在添加未初始化内存的资源。如果抛出,您仍然有一个默认构造的X~X()通过什么都不做正确处理。

现在添加第二个资源:x的构建副本。如果抛出,~X()仍将释放data_缓冲区,但不运行任何~A()

如果第二个资源成功,请通过递增X size_操作将noexcept设置为有效状态。如果抛出此后的任何内容,~X()将正确清理长度为1的缓冲区。

现在尝试第三种资源:y的构建副本。如果该构造抛出,~X()将正确清理长度为1的缓冲区。如果它没有抛出,请通知*this它现在拥有一个长度为2的缓冲区。

使用此技术要求X是默认可构造的。例如,默认构造函数可以是私有的。或者您可以使用其他将X置于无资源状态的私有构造函数:

: X{moved_from_tag{}}

在C ++ 11中,如果你的X可以拥有无​​资源状态通常是一个好主意,因为这样你就可以拥有一个noexcept移动构造函数,它与所有类型的优点捆绑在一起(并且是不同职位的主题。)

C ++ 11委托构造函数是一种非常好(可伸缩)的技术,用于编写异常安全构造函数,只要您在开始时具有无资源状态(例如,noexcept默认构造函数)。

是的,有很多方法可以在C ++ 98/03中实现,但它们并不漂亮。您必须创建一个X的实现细节基类,其中包含X的销毁逻辑,但不包含构造逻辑。去过那里,做到了,我喜欢委托施工人员。

答案 1 :(得分:7)

我认为问题源于违反单一责任原则:X类必须处理管理多个对象的生命周期(这可能不是它的主要责任)。

类的析构函数应该只释放类直接获取的资源。如果类只是一个复合(即类的实例拥有其他类的实例),理想情况下它应该依赖于自动内存管理(通过RAII)并且只使用默认的析构函数。如果类必须手动管理一些专用资源(例如打开文件描述符或连接,获取锁或分配内存),我建议将管理这些资源的责任分解为专用于此目的的类,然后使用该班级为会员。

使用标准模板库实际上会有所帮助,因为它包含专门处理此问题的数据结构(例如智能指针和std::vector<T>)。它们也是可复制的,因此即使您的X必须包含具有复杂资源获取策略的多个对象实例,也会针对每个成员以及包含复合类X来解决异常安全方式的资源管理问题。 / p>

答案 2 :(得分:1)

在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}
    {
    }

    // ...
};