14

クラス階層で非常によくある間違いの 1 つは、継承チェーン内のすべてのオーバーライドが何らかの作業を行うために、基本クラスのメソッドを仮想として指定し、呼び出しを基本実装に伝達するのを忘れることです。

シナリオ例

class Container
{
public:
  virtual void PrepareForInsertion(ObjectToInsert* pObject)
  {
    // Nothing to do here
  }
};

class SpecializedContainer : public Container
{
protected:
  virtual void PrepareForInsertion(ObjectToInsert* pObject)
  {
    // Set some property of pObject and pass on.
    Container::PrepareForInsertion(pObject);
  }
};

class MoreSpecializedContainer : public SpecializedContainer
{
protected:
  virtual void PrepareForInsertion(ObjectToInsert* pObject)
  {
    // Oops, forgot to propagate!
  }
};

私の質問は次のとおりです。基本実装が常に呼び出しチェーンの最後で呼び出されるようにするための良い方法/パターンはありますか?

これを行うための2つの方法を知っています。

方法 1

メンバー変数をフラグとして使用し、仮想メソッドの基本実装で正しい値に設定し、呼び出し後にその値を確認できます。これには、パブリックな非仮想メソッドをクライアントのインターフェイスとして使用し、仮想メソッドを保護する必要があります (これは実際には良いことです) が、この目的専用のメンバー変数を使用する必要があります (これには、仮想メソッドが const でなければならない場合は変更可能)。

class Container
{
public:
  void PrepareForInsertion(ObjectToInsert* pObject)
  {
    m_callChainCorrect = false;
    PrepareForInsertionImpl(pObject);
    assert(m_callChainCorrect);
  }

protected:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject)
  {
    m_callChainCorrect = true;
  }

private:
  bool m_callChainCorrect;
};

class SpecializedContainer : public Container
{
protected:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject)
  {
    // Do something and pass on
    Container::PrepareForInsertionImpl(pObject);
  }
};

方法 2

もう 1 つの方法は、メンバー変数を不透明な「Cookie」パラメーターに置き換えて、同じことを行うことです。

class Container
{
public:
  void PrepareForInsertion(ObjectToInsert* pObject)
  {
    bool callChainCorrect = false;
    PrepareForInsertionImpl(pObject, &callChainCorrect);
    assert(callChainCorrect);
  }

protected:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject, void* pCookie)
  {
    *reinrepret_cast<bool*>(pCookie) = true;
  }
};

class SpecializedContainer : public Container
{
protected:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject, void* pCookie)
  {
    // Do something and pass on
    Container::PrepareForInsertionImpl(pObject, pCookie);
  }
};

私の意見では、このアプローチは最初のアプローチよりも劣っていますが、専用のメンバー変数の使用を回避しています。

他にどのような可能性がありますか?

4

7 に答える 7

22

クラスを肥大化させ、オブジェクトの責任ではなくプログラマーの欠陥に対処するコードを追加するという代償を払って、これを行うためのいくつかの巧妙な方法を思いつきました。

本当の答えは、実行時にこれを行わないことです。これはランタイム エラーではなく、プログラマ エラーです。

コンパイル時に実行します。言語がサポートしている場合は言語構造を使用するか、それを強制するパターン (たとえば、テンプレート メソッド) を使用するか、コンパイルをテストの合格に依存させ、それを強制するテストを設定します。

または、伝播に失敗したために派生クラスが失敗した場合は、失敗させて、派生クラスの作成者に基本クラスを正しく使用できなかったことを通知する例外メッセージを表示します。

于 2009-08-13T17:26:10.160 に答える
6

継承のレベルが 1 つしかない場合は、パブリック インターフェイスが非仮想であり、仮想実装関数を呼び出すテンプレート メソッド パターンを使用できます。次に、ベースのロジックは、確実に呼び出されるパブリック関数に入ります。

複数レベルの継承があり、各クラスがその基本クラスを呼び出すようにしたい場合でも、テンプレート メソッド パターンを使用できますが、ひねりを加えて、仮想関数の戻り値を によってのみ構築 可能にすることで、強制的に呼び出すことができますbasederived値を返すための基本実装 (コンパイル時に強制)。

これは、各クラスが直接の基本クラスを呼び出すことを強制するものではなく、レベルをスキップする可能性があります (それを強制する良い方法は思いつきません) が、プログラマーに意識的な決定を強制します。つまり、機能します。悪意ではなく不注意に対して。

class base {
protected:
    class remember_to_call_base {
        friend base;
        remember_to_call_base() {} 
    };

    virtual remember_to_call_base do_foo()  { 
        /* do common stuff */ 
        return remember_to_call_base(); 
    }

    remember_to_call_base base_impl_not_needed() { 
        // allow opting out from calling base::do_foo (optional)
        return remember_to_call_base();
    }

public:
    void foo() {
        do_foo();
    }
};

class derived : public base  {

    remember_to_call_base do_foo()  { 
        /* do specific stuff */
        return base::do_foo(); 
    }
};

public(非) 関数が値を返す必要がvirtualある場合、内側の関数はvirtualreturn std::pair<-type ,を返す必要がありremember_to_call_base>ます。


注意事項:

  1. remember_to_call_baseprivate と宣言された明示的なコンストラクターがあるため、そのfriend(この場合baseは ) のみがこのクラスの新しいインスタンスを作成できます。
  2. remember_to_call_base 明示的に定義されたコピーコンストラクターがないためコンパイラーは、実装publicから値で返すことができるアクセシビリティを備えたコンストラクターを作成します。base
  3. remember_to_call_baseprotectedのセクションで宣言されている場合、セクションにbaseある場合はまったく参照できません。privatederived
于 2009-08-13T17:59:05.587 に答える
0

仮想関数を非表示にしてインターフェイスを非仮想にすることができることがわかった場合は、他のユーザーが関数を呼び出したかどうかを確認するのではなく、自分で呼び出すだけにしてみてください。最後にベースコードを呼び出す必要がある場合は、次のようになります。

class Container
{
public:
  void PrepareForInsertion(ObjectToInsert* pObject)
  {
    PrepareForInsertionImpl(pObject);
    doBasePreparing(pObject);
  }

protected:
  virtual void PrepareForInsertionImpl(ObjectToInsert* pObject)
  {
    // nothing to do
  }

private:
  void doBasePreparing(ObjectToInsert* pObject)
  {
    // put here your code from Container::PrepareForInsertionImpl
  }
};
于 2009-08-13T19:32:37.797 に答える
0

解決策の 1 つは、仮想メソッドをまったく使用せず、代わりにユーザーがコールバックを登録できるようにし、prepareForInsertion の作業を行う前にそれらを呼び出すことです。このようにして、コールバックと通常の処理の両方が確実に行われるようにするのは基本クラスであるため、その間違いを犯すことは不可能になります。多くの関数でこの動作が必要な場合は、多くのコールバックが発生する可能性があります。そのパターンを本当に頻繁に使用する場合は、この種のことを自動化できる AspectJ (または C# に相当するもの) などのツールを検討することをお勧めします。

于 2009-08-13T17:40:49.720 に答える
0

テンプレートメソッド patternを見てください。(基本的な考え方は、もう基本クラスのメソッドを呼び出す必要がないということです。)

于 2009-08-13T17:25:45.170 に答える