27

C++ と Java は、メソッドをオーバーライドするときに戻り値の型の共分散をサポートします。

ただし、どちらもパラメーター型の反分散をサポートしていません。代わりに、オーバーロード(Java) または非表示 (C++) に変換されます。

なぜですか?それを許しても害はないように思えます。その理由の 1 つを Java で見つけることができます - とにかくオーバーロードするための "choose-the-most-specific-version" メカニズムがあるため - しかし、C++ の理由は考えられません。

例 (Java):

class A {
    public void f(String s) {…}
}
class B extends A {
    public void f(Object o) {…} // Why doesn't this override A.f?
}
4

6 に答える 6

24

反分散の純粋な問題について

言語に反分散を追加すると、多くの潜在的な問題や不潔なソリューションが開かれ、言語サポートなしで簡単にシミュレートできるため、利点はほとんどありません。

struct A {};
struct B : A {};
struct C {
   virtual void f( B& );
};
struct D : C {
   virtual void f( A& );     // this would be contravariance, but not supported
   virtual void f( B& b ) {  // [0] manually dispatch and simulate contravariance
      D::f( static_cast<A&>(b) );
   }
};

単純な余分なジャンプを使用すると、反分散をサポートしない言語の問題を手動で克服できます。この例では、f( A& )は仮想である必要はなく、呼び出しは仮想ディスパッチ メカニズムを禁止するために完全に修飾されています。

このアプローチは、完全な動的ディスパッチを持たない言語に反分散を追加するときに発生する最初の問題の 1 つを示しています。

// assuming that contravariance was supported:
struct P {
   virtual f( B& ); 
};
struct Q : P {
   virtual f( A& );
};
struct R : Q {
   virtual f( ??? & );
};

反変性が有効な場合、Q::fは のオーバーライドとなり、 の引数になり得るP::fすべてのオブジェクトについて、同じオブジェクトの有効な引数であるため、これで問題ありません。ここで、階層に余分なレベルを追加することで、設計上の問題が発生します:有効なオーバーライドはありますか?oP::fQ::fR::f(B&)P::fR::f(A&)

署名が完全に一致するため、反変性なしR::f( B& )は明らかに のオーバーライドです。反変性を中間レベルに追加すると、レベルでは有効であるが、レベルまたはレベルのいずれでもないP::f引数が存在するという問題が生じます。要件を満たすための唯一の選択肢は、署名を強制的に にすることです。これにより、次のコードをコンパイルできます。QPRRQR::f( A& )

int main() {
   A a; R r;
   Q & q = r;
   q.f(a);
}

同時に、言語には次のコードを禁止するものは何もありません。

struct R : Q {
   void f( B& );    // override of Q::f, which is an override of P::f
   virtual f( A& ); // I can add this
};

これで面白い効果が得られました:

int main() {
  R r;
  P & p = r;
  B b;
  r.f( b ); // [1] calls R::f( B& )
  p.f( b ); // [2] calls R::f( A& )
}

[1] では、 のメンバー メソッドへの直接呼び出しがありRます。rは参照やポインタではなくローカル オブジェクトであるため、動的なディスパッチ メカニズムはなく、最適な一致はですR::f( B& )。同時に、[2] では、基本クラスへの参照を介して呼び出しが行われ、仮想ディスパッチ メカニズムが作動します。

R::f( A& )のオーバーライドは のQ::f( A& )オーバーライドであるためP::f( B& )、コンパイラは を呼び出す必要がありますR::f( A& )。これは言語で完全に定義できますが、2 つのほぼ正確な呼び出し [1] と [2] が実際には異なるメソッドを呼び出し、[2] ではシステムが最適ではない引数。

もちろん、それは別の議論になる可能性があります:R::f( B& )は正しいオーバーライドであるべきであり、R::f( A& ). この場合の問題は次のとおりです。

int main() {
   A a; R r;
   Q & q = r;
   q.f( a );  // should this compile? what should it do?
}

クラスを確認するQと、前のコードは完全に正しいです: as 引数Q::fを取ります。A&コンパイラがそのコードについて文句を言う理由はありません。しかし、問題は、この最後の仮定の下では、 as 引数ではなくR::fa を取ることです! 呼び出しの場所でのメソッドの署名が完全に正しいように見えても、配置される実際のオーバーライドは引数を処理できません。このパスにより、2 番目のパスが最初のパスよりもはるかに悪いことがわかります。をオーバーライドすることはできません。B&A&aR::f( B& )Q::f( A& )

最小の驚きの原則に従うと、コンパイラの実装者とプログラマの両方にとって、関数の引数に逆の差異を持たない方がはるかに簡単です。実現不可能だからではなく、コードに癖や驚きがあり、機能が言語に存在しない場合の簡単な回避策があることを考慮するためです。

過負荷と非表示について

Java と C++ の両方で、最初の例 ( ABCおよびD) で、手動ディスパッチ [0] を削除します。これらは異なるシグネチャであり、オーバーライドではありませんC::fD::fどちらの場合も、実際には同じ関数名のオーバーロードですが、C++ ルックアップ規則により、C::fオーバーロードは によって隠されるというわずかな違いがありD::fます。ただし、これは、コンパイラがデフォルトで隠しオーバーロードを検出しないことを意味するだけであり、存在しないということではありません。

int main() {
   D d; B b;
   d.f( b );    // D::f( A& )
   d.C::f( b ); // C::f( B& )
}

また、クラス定義を少し変更するだけで、Java とまったく同じように動作させることができます。

struct D : C {
   using C::f;           // Bring all overloads of `f` in `C` into scope here
   virtual void f( A& );
};
int main() {
   D d; B b;
   d.f( b );  // C::f( B& ) since it is a better match than D::f( A& )
}
于 2010-06-09T23:03:06.427 に答える
16
class A {
    public void f(String s) {...}
    public void f(Integer i) {...}
}

class B extends A {
    public void f(Object o) {...} // Which A.f should this override?
}
于 2010-06-08T11:38:00.017 に答える
5

C++ の場合、Stroustrup はThe Design & Evolution of C++ のセクション 3.5.3 で非表示にする理由について簡単に説明しています。彼の推論は (言い換えれば) 他のソリューションも同様に多くの問題を引き起こし、それは C With Classes の時代から続いているというものです。

例として、彼は 2 つのクラスと派生クラス B を示しています。両方とも、それぞれの型のポインターを受け取る virtual copy() 関数を持っています。私たちが言うなら:

A a;
B b;
b.copy( & a );

B の copy() が A を隠しているため、これは現在エラーです。エラーでなければ、A の copy() 関数で更新できるのは B の A 部分だけです。

もう一度言いますが、もし興味があれば、すばらしい本を読んでください。

于 2010-06-08T09:08:59.057 に答える
3

これはどの oo 言語でも便利な機能ですが、現在の仕事に適用できるかどうかはまだわかりません。

ひょっとしたら、その必要性はあまりないのかもしれません。

于 2010-06-08T09:11:58.010 に答える
2

上記の回答をくれた Donroby に感謝します。

interface Alpha
interface Beta
interface Gamma extends Alpha, Beta
class A {
    public void f(Alpha a)
    public void f(Beta b)
}
class B extends A {
    public void f(Object o) {
        super.f(o); // What happens when o implements Gamma?
    }
}

複数の実装の継承が推奨されない理由に似た問題に陥っています。(Af(g) を直接呼び出そうとすると、コンパイル エラーが発生します。)

于 2010-06-08T12:12:21.163 に答える
1

donroby と David の回答のおかげで、パラメーターの反分散を導入する際の主な問題は、オーバーロード メカニズムとの統合であることを理解していると思います。

したがって、複数のメソッドに対する単一のオーバーライドに問題があるだけでなく、他の方法でも問題があります。

class A {
    public void f(String s) {...}
}

class B extends A {
    public void f(String s) {...} // this can override A.f
    public void f(Object o) {...} // with contra-variance, so can this!
}

そして、同じメソッドに対して 2 つの有効なオーバーライドがあります。

A a = new B();
a.f(); // which f is called?

オーバーロードの問題以外に、他に何も考えられませんでした。

編集:私はそれ以来、上記に同意するこの C++ FQA エントリ (20.8)を見つけました - オーバーロードの存在は、パラメーターの反分散に深刻な問題を引き起こします。

于 2010-06-10T10:32:42.590 に答える