web-dev-qa-db-ja.com

例外安全な方法で複数のリソースを取得する必要があるコンストラクターを処理する方法

複数のリソースを所有する重要な型があります。例外安全な方法でそれを構築するにはどうすればよいですか?

たとえば、次は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を使用する」という適切なアドバイスを使用するのは不便な、より複雑なシナリオの代用にすぎません。このデータ構造に精通しているという理由だけで、このデータ構造を使用して質問を伝えることにしました。

crystalを明確にする:デフォルトの~X()がすべてを適切にクリーンアップするようにXを設計できる場合( 「ゼロのルール」)、または~X()が単一のリソースを解放するだけでよい場合は、それが最適です。ただし、実際には~X()が複数のリソースを処理する必要がある場合があり、この質問はそれらの状況に対処します。

したがって、この型にはすでに優れたデストラクタと優れたデフォルトコンストラクタがあります。私の質問は、2つの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 const& a): 1の結果である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?!コンパイル時にリソースの数が不明である場合はどうなりますか?!

より良い方法はありますか?

32
Howard Hinnant

より良い方法はありますか?

[〜#〜]はい[〜#〜]

C++ 11は、この状況veryを適切に処理する委任コンストラクターと呼ばれる新機能を提供します。しかし、それは少し微妙です。

コンストラクターで例外をスローする場合の問題は、コンストラクターが完了するまで、作成しているオブジェクトのデストラクタが実行されないことを理解することです。サブオブジェクト(ベースとメンバー)のデストラクタは、例外がスローされた場合に実行されますが、それらのサブオブジェクトが完全に構築されるとすぐに実行されます。

ここで重要なのは、Xを完全に構築してからにリソースを追加し、次にリソースを追加することです一度に1つずつ、各リソースを追加するときに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では、次のように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_;
}
_

これは、例外の安全性を完全に知らないコードを書くのとほとんど同じです。違いはこの行です:

_    : X{}
_

これは言う:私にデフォルトのXを構築する。この構築の後、_*this_が完全に構築され、後続の操作で例外がスローされると、~X()が実行されます。 これは革命的です!

この場合、デフォルトで構築されたXはリソースを取得しないことに注意してください。確かに、それは暗黙的にnoexceptですらあります。その部分はスローされません。そして、_*this_をサイズ0の配列を保持する有効なXに設定します。~X()はその状態を処理する方法を知っています。

次に、初期化されていないメモリのリソースを追加します。それがスローされた場合でも、デフォルトで構築されたXがあり、~X()は何もしないことでそれを正しく処理します。

次に、2番目のリソースを追加します:xの構築されたコピー。それがスローされた場合でも、~X()は_data__バッファーの割り当てを解除しますが、~A()は実行しません。

2番目のリソースが成功した場合は、X操作である_size__をインクリメントして、noexceptを有効な状態に設定します。この後に何かがスローされた場合、~X()は長さ1のバッファーを正しくクリーンアップします。

次に、3番目のリソースであるyの構築されたコピーを試してください。その構造がスローされた場合、~X()は長さ1のバッファーを正しくクリーンアップします。スローされない場合は、長さ2のバッファーを所有していることを_*this_に通知します。

この手法を使用する場合、デフォルトで構成可能であるためにXは必要ありません。たとえば、デフォルトのコンストラクターはプライベートにすることができます。または、Xをリソースのない状態にする他のプライベートコンストラクターを使用することもできます。

_: X{moved_from_tag{}}
_

C++ 11では、Xがリソースのない状態になる可能性がある場合は、一般的に良い考えです。これにより、あらゆる種類の機能がバンドルされたnoexcept移動コンストラクターを使用できるようになります(別の投稿の件名です)。

C++ 11の委任コンストラクターは、最初に構築するリソースのない状態(たとえば、noexceptデフォルトコンストラクター)がある限り、例外安全コンストラクターを作成するための非常に優れた(スケーラブルな)手法です。

はい、C++ 98/03でこれを行う方法はありますが、それほどきれいではありません。 Xの破棄ロジックを含み、構築ロジックを含まないXの実装詳細基本クラスを作成する必要があります。そこに行って、それをやった、私はコンストラクターを委任するのが大好きです。

35
Howard Hinnant

この問題は、単一責任の原則の違反に起因すると思います。クラスXは、複数のオブジェクトの存続期間の管理に対処する必要があります(おそらく、それが主な責任ではありません)。

クラスのデストラクタは、クラスが直接取得したリソースのみを解放する必要があります。クラスが単なる複合である場合(つまり、クラスのインスタンスが他のクラスのインスタンスを所有している場合)、理想的には(RAIIを介した)自動メモリ管理に依存し、デフォルトのデストラクタを使用する必要があります。クラスがいくつかの特殊なリソースを手動で管理する必要がある場合(たとえば、ファイル記述子または接続を開く、ロックを取得する、またはメモリを割り当てる)、これらのリソースをこの目的専用のクラスに管理する責任を除外し、次のインスタンスを使用することをお勧めします。メンバーとしてのそのクラス。

標準のテンプレートライブラリを使用すると、この問題を排他的に処理するデータ構造(スマートポインタやstd::vector<T>など)が含まれているため、実際に役立ちます。それらも可能であるため、Xに複雑なリソース取得戦略を持つオブジェクトの複数のインスタンスを含める必要がある場合でも、例外安全な方法でのリソース管理の問題は、各メンバーとそれを含む複合クラスXの両方で解決されます。

7
Victor Savu

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

    // ...
};
1
Remy Lebeau