2

型が共通の基本クラスから派生した 2 つのオブジェクト間の相互作用を実装したいと考えています。デフォルトの相互作用があり、同じタイプのオブジェクトが相互作用すると特定のことが起こる場合があります。これは、次の二重ディスパッチ スキームを使用して実装されます。

#include <iostream>

class A
{
public:
  virtual void PostCompose(A* other)
    {
      other->PreCompose(this);
    }
  virtual void PreCompose(A* other)
    {
      std::cout << "Precomposing with an A object" << std::endl;
    }
};

class B : public A
{
public:
  virtual void PostCompose(A* other) // This one needs to be present to prevent a warning
    {
      other->PreCompose(this);
    }
  virtual void PreCompose(A* other) // This one needs to be present to prevent an error
    {
      std::cout << "Precomposing with an A object" << std::endl;
    }
  virtual void PostCompose(B* other)
    {
      other->PreCompose(this);
    }
  virtual void PreCompose(B* other)
    {
      std::cout << "Precomposing with a B object" << std::endl;
    }
};

int main()
{
  A a;
  B b;
  a.PostCompose(&a); // -> "Precomposing with an A object"
  a.PostCompose(&b); // -> "Precomposing with an A object"
  b.PostCompose(&a); // -> "Precomposing with an A object"
  b.PostCompose(&b); // -> "Precomposing with a B object"
}

残念ながら、このコードに関してまったく異なる質問が 2 つあります。

  1. これは合理的なアプローチだと思いますか?何か違うことを提案しますか?
  2. 最初の 2 つのメソッドを省略すると、最後の 2 つのメソッドがメソッドを隠してBいるというコンパイラの警告とエラーが表示されます。何故ですか?ポインターをポインターにキャストするべきではありませんか?BAA*B*

更新:追加することがわかりました

using A::PreCompose;
using A::PostCompose;

エラーと警告が消えますが、なぜこれが必要なのですか?

更新 2 : これはここできちんと説明されています: http://www.parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.9、ありがとう。私の最初の質問はどうですか?このアプローチに関するコメントはありますか?

4

3 に答える 3

4

二重ディスパッチは通常、C++ では異なる方法で実装され、基底クラスにはすべて異なるバージョンがあります (これはメンテナンスの悪夢になりますが、それが言語のやり方です)。ディスパッチを二重にしようとする際の問題は、動的ディスパッチがBメソッドを呼び出しているオブジェクトの最も派生した型を見つけますが、引数には静的型があることA*です。A引数として受け取るオーバーロードがないためB*、呼び出しother->PreCompose(this)は暗黙的にアップキャストされ、2 番目の引数で単一のディスパッチが残されますthisA*

実際の質問の時点で:コンパイラが警告を生成するのはなぜですか? なぜusing A::Precomposeディレクティブを追加する必要があるのですか?

その理由は、C++ のルックアップ規則にあります。その後、コンパイラは への呼び出しに遭遇しobj.member()、識別子 をルックアップmemberする必要があり、 の静的タイプから開始してそうします。そのコンテキストでのobj位置を特定できなかった場合member、階層内で上に移動し、静的タイプのベースをルックアップします。のタイプobj

最初の識別子が見つかると、ルックアップは停止し、関数呼び出しを使用可能なオーバーロードと照合しようとします。呼び出しが一致しない場合は、エラーが発生します。ここで重要な点は、関数呼び出しが一致しない場合、ルックアップは階層内でそれ以上検索しないということです。宣言を追加するusing base::memberことで、識別子memberを基本クラスから現在のスコープに取り込みます。

例:

struct base {
   void foo( const char * ) {}
   void foo( int ) {}
};
struct derived : base {
   void foo( std::string const & ) {};
};
int main() {
   derived d;
   d.foo( "Hi" );
   d.foo( 5 );
   base &b = d;
   b.foo( "you" );
   b.foo( 5 );
   d.base::foo( "there" );
}

コンパイラが式d.foo( "Hi" );the static type of the object isderivedを検出すると、ルックアップは 内のすべてのメンバー関数をチェックしderived、識別子fooはそこにあり、ルックアップは上に進みません。唯一の利用可能なオーバーロードへの引数はstd::string const&であり、コンパイラは暗黙的な変換を追加するため、最適な潜在的な一致 (base::foo(const char*)はその呼び出しよりも適切な一致) がある場合でも、derived::foo(std::string const&)効果的に呼び出します:

d.derived::foo( std::string("Hi") );

次の式d.foo( 5 );も同様に処理され、検索が開始されderived、そこにメンバー関数があることがわかります。ただし、引数5を暗黙的に に変換することはできずstd::string const &、 に完全に一致する場合でも、コンパイラはエラーを発行しますbase::foo(int)。これは、クラス定義のエラーではなく、呼び出しのエラーであることに注意してください。

3 番目の式を処理するときb.foo( "you" );、オブジェクトの静的な型はbase(実際のオブジェクトは ですderivedが、参照の型は であることに注意してくださいbase&) であるため、ルックアップは を検索せずに をderived開始しbaseます。2 つのオーバーロードが見つかり、そのうちの 1 つが適切に一致するため、 が呼び出されますbase::foo( const char* )。についても同様ですb.foo(5)

最後に、最も派生したクラスにさまざまなオーバーロードを追加すると、ベースのオーバーロードが隠されますが、オブジェクトからは削除されないため、呼び出しを完全に修飾することで必要なオーバーロードを実際に呼び出すことができます (これにより、ルックアップが無効になり、関数が仮想の場合に動的ディスパッチをスキップするという副作用が追加されたためd.base::foo( "there" )、ルックアップはまったく実行されず、 への呼び出しのみがディスパッチされbase::foo( const char* )ます。

クラスにusing base::foo宣言を追加した場合、 inの使用可能なオーバーロードにinのすべてのオーバーロードを追加すると、呼び出しでin のオーバーロードが考慮され、最適なオーバーロードがであることが判明するため、実際には次のように実行されます。derivedfoobasederivedd.foo( "Hi" );basebase::foo( const char* );d.base::foo( "Hi" );

多くの場合、開発者はルックアップ ルールが実際にどのように機能するかを常に考えているわけではありません。宣言d.foo( 5 );なしで への呼び出しが失敗したり、さらに悪いことに、 への呼び出しが明らかに より悪いオーバーロードであるときに にディスパッチされたりすると、驚くかもしれません。これが、メンバー関数を非表示にするとコンパイラが警告する理由の 1 つです。この警告のもう 1 つの正当な理由は、多くの場合、実際に仮想関数をオーバーライドしようとしたときに、誤って署名を変更してしまう可能性があることです。using base::food.foo( "Hi" );derived::foo( std::string const & )base::foo( const char* )

struct base {
   virtual std::string name() const {
      return "base";
   };
};
struct derived : base {
   virtual std::string name() {        // missing const!!!!
      return "derived";
   }
}
int main() {
   derived d; 
   base & b = d;
   std::cout << b.name() << std::endl; // "base" ????
}

メンバー関数をオーバーライドしようとしているときの小さな間違いname(修飾子を忘れるconst) は、実際には別の関数シグネチャを作成していることを意味します。へのオーバーライドderived::nameではないため、への参照を介した へbase::nameの呼び出しは!!!にディスパッチされません。namebasederived::name

于 2011-04-14T10:06:59.193 に答える
1
using A::PreCompose;
using A::PostCompose;
makes the errors and warnings vanish, but why is this necessary?

基本クラスに含まれるのと同じ名前で新しい関数を派生クラスに追加し、基本クラスから仮想関数をオーバーライドしない場合、新しい名前は基本クラスから古い名前を隠します。

そのため、明示的に記述してそれらを再表示する必要があります。

using A::PreCompose;
using A::PostCompose;

それらを非表示にする他の方法(この特定のケースでは)は、投稿したコードで行った基本クラスの仮想関数をオーバーライドすることです。コードは問題なくコンパイルされると思います。

于 2011-04-14T09:28:21.957 に答える
0

クラスはスコープであり、基本クラスでのルックアップは、囲んでいるスコープでのルックアップとして記述されます。

関数のオーバーロードを検索するとき、ネストされた関数で関数が見つかった場合、囲んでいるスコープでの検索は行われません。

2 つのルールの結果は、実験した動作です。using 句を追加すると、外側のスコープから定義がインポートされ、通常の解決策になります。

于 2011-04-14T09:28:37.863 に答える