为什么没有参数反差方法可以覆盖?

重写方法时,C ++和Java支持返回类型协方差。

但是,它们都不支持参数类型中的反差 – 相反,它转换为过 (Java)或隐藏(C ++)。

那是为什么 ? 在我看来,允许这样做是没有害处的。 我可以在Java中找到它的一个原因 – 因为它无论如何都有“选择最特定版本”的重载机制 – 但是不能想到C ++的任何原因。

示例(Java):

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

关于反方差的纯问题

在语言中添加逆变量可以解决许多潜在问题或不清洁的解决方案,并且提供的优势很少,因为它可以在没有语言支持的情况下轻松模拟:

 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(b) ); } }; 

通过简单的额外跳转,您可以手动克服不支持反差的语言问题。 在该示例中, f( A& )不需要是虚拟的,并且该调用完全限定以禁止虚拟调度机制。

这种方法显示了在对没有完全动态调度的语言添加逆方差时出现的第一个问题:

 // 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的重写,对于每个可以作为P::f的参数的对象o都可以,同一个对象 Q::f的有效参数Q::f 。 现在,通过在层次结构中添加额外的级别,我们最终会遇到设计问题: R::f(B&)P::f的有效覆盖还是R::f(A&)

没有逆变R::f( B& )显然是P::f的重写,因为签名是完美的匹配。 一旦你向中间水平添加了逆向性,问题就是存在在Q水平上有效但不在PR水平的参数。 要使R满足Q要求,唯一的选择是强制签名为R::f( A& ) ,以便以下代码可以编译:

 int main() { A a; R r; Q & q = r; qf(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; rf( b ); // [1] calls R::f( B& ) pf( 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& ) 。 虽然这可以在语言中完美地定义,但是可能会惊奇地发现两个几乎完全的调用[1]和[2]实际上调用了不同的方法,并且在[2]中系统将调用匹配的最佳匹配争论。

当然,它可以有所不同: R::f( B& )应该是正确的覆盖,而不是R::f( A& ) 。 这种情况下的问题是:

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

如果检查Q类,前面的代码是完全正确的: Q::f采用A& as参数。 编译器没有理由抱怨该代码。 但问题是,在这最后的假设下, R::f采用B&而不是A&作为参数! 即使在调用地方的方法的签名看起来完全正确,实际的覆盖将无法处理参数。 这条路径使我们确定第二条路径比第一条路径差得多。 R::f( B& )不可能是Q::f( A& )的重写。

遵循最少意外的原则,对于编译器实现者和程序员来说,在函数参数中不具有矛盾方差要简单得多。 不是因为它不可行,而是因为代码中存在怪癖和惊喜,并且考虑到如果该语言中不存在该function,则存在简单的解决方法。

关于重载与隐藏

在Java和C ++中,在第一个示例(使用ABCD )中删除手动调度[0], C::fD::f是不同的签名而不是覆盖。 在这两种情况下,它们实际上都是相同函数名的重载,但由于C ++查找规则的原因, C::f重载将由D::f隐藏。 但这只意味着编译器默认情况下不会找到隐藏的重载,而不是它不存在:

 int main() { D d; B b; df( b ); // D::f( A& ) dC::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; df( b ); // C::f( B& ) since it is a better match than D::f( A& ) } 
 class A { public void f(String s) {...} public void f(Integer i) {...} } class B extends A { public void f(Object o) {...} // Which Af should this override? } 

对于C ++,Stroustrup在C ++ 的设计和演变的第3.5.3节中简要讨论了隐藏的原因。 他的推理是(我解释)其他解决方案引发了同样多的问题,而且自从C With Classes天以来就是这样。

作为一个例子,他给出了两个类 – 和一个派生类B.两个都有一个虚拟的copy()函数,它接受各自类型的指针。 如果我们说:

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

这是一个错误,因为B的副本()隐藏了A. 如果不是错误,只能通过A的copy()函数更新B的A部分。

再一次,我已经解释了 – 如果你有兴趣,请阅读这本书,这本书很棒。

虽然这在任何oo语言中都是很好的,但我仍然需要在我目前的工作中遇到它的适用性。

也许真的不需要它。

感谢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? } } 

您遇到的问题类似于不鼓励多个实现inheritance的原因。 (如果您尝试直接调用Af(g),则会出现编译错误。)

感谢donroby和David的回答,我想我明白引入参数反差的主要问题是与重载机制集成

因此,不仅多个方法的单个覆盖存在问题,而且另一种方式:

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

现在,同一方法有两个有效的覆盖:

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

除了超载问题,我想不出别的什么。

编辑:我已经发现这个C ++ FQA条目(20.8)与上述一致 – 存在重载会导致参数反差的严重问题。