32

私はRAIIの利点に精通していますが、最近、次のようなコードの問題に遭遇しました。

class Foo
{
  public:
  Foo()
  {
    DoSomething();
    ...     
  }

  ~Foo()
  {
    UndoSomething();
  } 
} 

...コンストラクターセクションのコードが例外をスローし、結果UndoSomething()が呼び出されなかったことを除いて、すべて問題ありません。

その特定の問題を修正する明らかな方法があります。たとえば...、try / catchブロックをラップしてから呼び出すUndoSomething()などですが、a:コードを複製し、b:try / catchブロックは、RAII手法を使用して回避しようとするコードの臭いです。また、複数のDo / Undoペアが含まれている場合、コードは悪化し、エラーが発生しやすくなる可能性があり、途中でクリーンアップする必要があります。

これを行うためのより良いアプローチがあるのではないかと思います-おそらく、別のオブジェクトが関数ポインターを受け取り、それが破壊されたときに関数を呼び出しますか?

class Bar 
{
  FuncPtr f;
  Bar() : f(NULL)
  {
  }

  ~Bar()
  {
    if (f != NULL)
      f();
  }
}   

私はそれがコンパイルされないことを知っていますが、それは原理を示すはずです。Fooはその後...

class Foo
{
  Bar b;

  Foo()
  {
    DoSomething();
    b.f = UndoSomething; 
    ...     
  }
}

fooはデストラクタを必要としないことに注意してください。それは価値があるよりも厄介なように聞こえますか、それともこれは私にとって重い持ち上げを処理するためのブーストに役立つ何かを備えたすでに一般的なパターンですか?

4

6 に答える 6

31

問題は、クラスがやりすぎだということです。RAIIの原則は、リソースを(コンストラクターまたはそれ以降で)取得し、デストラクタがそれを解放することです。クラスは、そのリソースを管理するためだけに存在します。

あなたの場合、クラス自体ではなくDoSomething()UndoSomething()クラスのユーザーの責任である必要があります。

Steve Jessopがコメントで述べているように、取得するリソースが複数ある場合は、それぞれを独自のRAIIオブジェクトで管理する必要があります。そして、これらを順番にそれぞれを構築する別のクラスのデータメンバーとして集約することは理にかなっているかもしれません。その後、取得が失敗した場合、以前に取得したすべてのリソースは、個々のクラスメンバーのデストラクタによって自動的に解放されます。

(また、三つのルールを覚えておいてください。クラスは、コピーを防ぐか、適切な方法で実装して、への複数の呼び出しを防ぐ必要がありますUndoSomething())。

于 2012-07-05T14:20:13.537 に答える
17

DoSomething/UndoSomethingを適切なRAIIハンドルにするだけです。

struct SomethingHandle
{
  SomethingHandle()
  {
    DoSomething();
    // nothing else. Now the constructor is exception safe
  }

  SomethingHandle(SomethingHandle const&) = delete; // rule of three

  ~SomethingHandle()
  {
    UndoSomething();
  } 
} 


class Foo
{
  SomethingHandle something;
  public:
  Foo() : something() {  // all for free
      // rest of the code
  }
} 
于 2012-07-05T14:22:37.790 に答える
6

私もRAIIを使用してこれを解決します:

class Doer
{
  Doer()
  { DoSomething(); }
  ~Doer()
  { UndoSomething(); }
};
class Foo
{
  Doer doer;
public:
  Foo()
  {
    ...
  }
};

doerは、ctor本体が開始する前に作成され、デストラクタが例外によって失敗した場合、またはオブジェクトが正常に破棄された場合に破棄されます。

于 2012-07-05T14:22:08.980 に答える
6

1つのクラスが多すぎます。DoSomething / UndoSomethingを別のクラス(「Something」)に移動し、そのクラスのオブジェクトをクラスFooの一部として持つと、次のようになります。

class Foo
{
  public:
  Foo()
  {
    ...     
  }

  ~Foo()
  {
  } 

  private:
  class Something {
    Something() { DoSomething(); }
    ~Something() { UndoSomething(); }
  };
  Something s;
} 

これで、Fooのコンストラクターが呼び出されるまでにDoSomethingが呼び出され、Fooのコンストラクターがスローされると、UndoSomethingが適切に呼び出されます。

于 2012-07-05T14:25:28.700 に答える
6

try / catchは一般的にコードの臭いではなく、エラーを処理するために使用する必要があります。ただし、あなたの場合は、エラーを処理せず、単にクリーンアップするだけなので、コードの臭いになります。それがデストラクタの目的です。

(1)コンストラクタが失敗したときにデストラクタ内のすべてを呼び出す必要がある場合は、デストラクタによって呼び出されるプライベートクリーンアップ関数に移動し、失敗した場合はコンストラクタに移動します。これはあなたがすでに行ったことのようです。よくできた。

(2)より良いアイデアは次のとおりです。別々に破壊できる複数のdo / undoペアがある場合、それらはミニタスクを実行し、それ自体の後でクリーンアップする独自の小さなRAIIクラスにラップする必要があります。オプションのクリーンアップポインタ関数を与えるというあなたの現在の考えは嫌いです。それは混乱を招くだけです。クリーンアップは常に初期化と組み合わせる必要があります。これがRAIIのコアコンセプトです。

于 2012-07-05T14:31:14.120 に答える
0

経験則:

  • クラスが何かの作成と削除を手動で管理している場合、それはやりすぎです。
  • クラスが手動でコピー割り当て/構築を記述している場合は、管理が多すぎる可能性があります
  • これに対する例外:1つのエンティティのみを管理することを唯一の目的とするクラス

3番目のルールstd::shared_ptrの例は、、、、、、、、およびもちろん以下のクラスです。std::unique_ptrscope_guardstd::vector<>std::list<>scoped_lockTrasher


補遺。

ここまで進んで、Cスタイルのものと相互作用する何かを書くことができます:

#include <functional>
#include <iostream>
#include <stdexcept>


class Trasher {
public:
    Trasher (std::function<void()> init, std::function<void()> deleter)
    : deleter_(deleter)
    {
        init();
    }

    ~Trasher ()
    {
        deleter_();
    }

    // non-copyable
    Trasher& operator= (Trasher const&) = delete;
    Trasher (Trasher const&) = delete;

private:
    std::function<void()> deleter_;
};

class Foo {
public:
    Foo ()
    : meh_([](){std::cout << "hello!" << std::endl;},
           [](){std::cout << "bye!"   << std::endl;})
    , moo_([](){std::cout << "be or not" << std::endl;},
           [](){std::cout << "is the question"   << std::endl;})
    {
        std::cout << "Fooborn." << std::endl;
        throw std::runtime_error("oh oh");
    }

    ~Foo() {
        std::cout << "Foo in agony." << std::endl;
    }

private:
    Trasher meh_, moo_;
};

int main () {
    try {
        Foo foo;
    } catch(std::exception &e) {
        std::cerr << "error:" << e.what() << std::endl;
    }
}

出力:

hello!
be or not
Fooborn.
is the question
bye!
error:oh oh

したがって、~Foo()実行されることはありませんが、初期化と削除のペアは実行されます。

良い点の1つは、init-function自体がスローされた場合、init-functionによってスローされた例外は直接通過Trasher()し、実行されないため、delete-functionは呼び出され~Trasher()ないということです。

注:最も外側にあることが重要ですtry/catch。そうでない場合、標準ではスタックの巻き戻しは必要ありません。

于 2012-07-06T11:27:12.380 に答える