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 A.f?
}
最佳答案
关于逆变的纯粹问题
向语言添加逆变会导致大量潜在问题或不干净的解决方案,并且提供的优势很小,因为它可以在没有语言支持的情况下轻松模拟:
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& )
不需要是虚拟的,调用完全符合条件来抑制虚拟调度机制。这种方法显示了向没有完全动态调度的语言添加逆变时出现的第一批问题之一:
// 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& )
.虽然这可以在语言中完美定义,但发现两个几乎完全相同的调用 [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
类,前面的代码完全正确:Q::f
需要一个 A&
作为论据。编译器没有理由提示该代码。但问题是,在最后一个假设下 R::f
需要一个 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& )
}
关于java - 为什么没有用于覆盖的参数逆变?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2995926/