8

GotW#8のタスクは、テンプレート引数のデストラクタのみがスローしないことを前提として、例外に依存しない汎用スタックデータ構造をC++で実装することです。秘訣は、スローされる可能性のあるテンプレート引数操作(コンストラクター、コピーコンストラクター、代入)を処理して、スタックがスローされた場合に一貫した状態のままにすることです。

解決策では、ハーブサッターは言います

このソリューションを単純にするために、例外セーフなリソース所有権の基本クラスの手法を示さないことにしました。

少しグーグルした後、1997年にさかのぼるDave Abrahamsによるこの答えを見つけました。彼のソリューションでは、彼は基本クラスのメモリの割り当てと削除を処理し、サブクラスのスタック操作を実装しています。このようにして、コピーコンストラクタでの要素のコピーがメモリ割り当てから分離されていることを確認します。これにより、コピーが失敗した場合は、何があっても基本クラスのデストラクタが呼び出されます。

参考までに、コメントを追加したDaveのコピーコンストラクターを次に示します。

// v_ refers to the internal array storing the stack elements
Stack(const Stack& rhs)
        : StackBase<T>( rhs.Count() ) // constructor allocates enough space
                                      // destructor calls delete[] appropriately
{
        while ( Count() < rhs.Count() )
           Push( rhs.v_[ Count() ] ); // may throw
}

基本コンストラクタが成功すると、サブクラスのコピーコンストラクタがスローした場合でも、基本デストラクタでのメモリのクリーンアップが保証されます。

私の質問は次のとおりです。

  1. 上で概説した場合を除いて、このアプローチに他の利点はありますか?
  2. 自分で問題を解決したときに、このコピーコンストラクターを思いつきました。

    // v_ refers to the internal array storing the stack elements
    // vsize_ is the amount of space allocated in v_
    // vused_ is the amount of space used so far in v_
    Stack (const Stack &rhs) :
            vsize_ (0), vused_ (0), v_ (0) {
        Stack temp (rhs.vused_); // constructor calls `new T[num_elements]`
                                 // destructor calls `delete[] v_`
        std::copy (rhs.v_, rhs.v_ + rhs.vused_, temp.v_); // may throw
        swap (temp);
    }
    void swap (Stack &rhs) {
        std::swap (v_, rhs.v_);
        std::swap (vused_, rhs.vused_);
        std::swap (vsize_, rhs.vsize_);
    }
    

    このアプローチと比較すると、基本クラスを持つのはやや面倒です。このtemp-copy-then-swapアプローチよりも基本クラスの手法を優先する必要がある理由はありますか?で使用しているため、Daveと私は両方ともすでにswap()メンバーを持っていることに注意してくださいoperator=()

  3. Dave Abrahamsのテクニックはあまりよく知られていないようです(Googleによると)。それは別の名前を持っていますか、それは標準的な習慣ですか、私は何かを逃しましたか?

ノート:

  • デイブPush()がループ内にいると、私の使用法と同等であると想定します。std::copy
  • スマートポインタを使用すると、この演習でメモリを明示的に管理する必要がなくなるため、スマートポインタを回答から除外しましょう。
4

1 に答える 1

1

動作上、2つの実装は同じです。どちらも、コンストラクターが失敗した場合にスコープの終了時にクリーンアップするマネージメモリ割り当てオブジェクトをセットアップします。一時変数へのコピーはよりコストがかかる可能性がありますが、コメントに記載されているように、std::moveおそらくそのような追加コストを無効にするでしょう。あなたの特定の質問に答えて:

  1. Abrahamによる例では、ヒープ割り当てを実際のクラス実装の詳細からさらに遠ざけています。コードでは、配列をコピーする前後に、より複雑なメモリ操作を行うと、すべてのエンティティを正しく管理することが少し難しくなる可能性があります。それ以外の場合、最初の実装の動作に関して、スタイルを超えてまだカバーしていない明確な詳細は表示されません。
  2. Abrahamの実装は、クリーンアップを単一の場所に抽象化します。複数のクラスが使用する場合StackBase<T>そうすれば、例外をスローした場合に動的メモリがクリーンアップされると安全に想定できます。実装では、同じことを実現するために、一時オブジェクト(の少なくとも一部)とスワップコードを書き直す必要があります。事実上、この実装は、StackBaseの複数のサブクラスを実装するための行数を減らします。ただし、他の基本クラスの上に追加のメモリ割り当てが必要な場合、実装では多重継承を回避します。あなたのコードはまた、コンパイル時間/サイズを膨らませるテンプレートコードを回避します-私は通常、これがほとんどの場合に関係なく大きなネガティブであるとは考えていませんが。非常に一般的なユースケースコードを作成しようとしない限り、デフォルトとしてコードに近いものを使用するでしょう。
  3. このアプローチに特定の名前があるかどうかはわかりませんが、見つかったら更新しますが、少なくとも1冊のC++プログラミングの本で使用されているのを見たことがあります。
于 2013-03-26T19:30:33.530 に答える