反分散の純粋な問題について
言語に反分散を追加すると、多くの潜在的な問題や不潔なソリューションが開かれ、言語サポートなしで簡単にシミュレートできるため、利点はほとんどありません。
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
すべてのオブジェクトについて、同じオブジェクトがの有効な引数であるため、これで問題ありません。ここで、階層に余分なレベルを追加することで、設計上の問題が発生します:有効なオーバーライドはありますか?o
P::f
Q::f
R::f(B&)
P::f
R::f(A&)
署名が完全に一致するため、反変性なしR::f( B& )
は明らかに のオーバーライドです。反変性を中間レベルに追加すると、レベルでは有効であるが、レベルまたはレベルのいずれでもないP::f
引数が存在するという問題が生じます。要件を満たすための唯一の選択肢は、署名を強制的に にすることです。これにより、次のコードをコンパイルできます。Q
P
R
R
Q
R::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::f
a を取ることです! 呼び出しの場所でのメソッドの署名が完全に正しいように見えても、配置される実際のオーバーライドは引数を処理できません。このパスにより、2 番目のパスが最初のパスよりもはるかに悪いことがわかります。をオーバーライドすることはできません。B&
A&
a
R::f( B& )
Q::f( A& )
最小の驚きの原則に従うと、コンパイラの実装者とプログラマの両方にとって、関数の引数に逆の差異を持たない方がはるかに簡単です。実現不可能だからではなく、コードに癖や驚きがあり、機能が言語に存在しない場合の簡単な回避策があることを考慮するためです。
過負荷と非表示について
Java と C++ の両方で、最初の例 ( A
、B
、C
およびD
) で、手動ディスパッチ [0] を削除します。これらは異なるシグネチャであり、オーバーライドではありませんC::f
。D::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& )
}