关于纯粹的逆变问题
为语言添加逆变器会带来很多潜在的问题或不干净的解决方案,并且几乎没有优势,因为它可以在没有语言支持的情况下轻松模拟:
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&
as 参数。编译器没有理由抱怨该代码。但问题是,在最后一个假设下,R::f
需要 aB&
而不是 aA&
作为论点!a
即使调用位置的方法签名看起来完全正确,实际的覆盖将无法处理参数。这条路径使我们确定第二条路径比第一条路径差得多。R::f( B& )
不可能覆盖Q::f( A& )
.
遵循最不意外的原则,对于编译器实现者和程序员来说,在函数参数中没有相反的变化要简单得多。不是因为它不可行,而是因为代码中会有怪癖和惊喜,并且考虑到如果语言中不存在该功能,则有简单的解决方法。
关于重载与隐藏
在 Java 和 C++ 中,在第一个示例中(使用A
、B
和)删除手动调度 [0],并且C
是不同的签名而不是覆盖。在这两种情况下,它们实际上都是相同函数名的重载,只是由于 C++ 查找规则,重载将被隐藏。但这仅意味着编译器默认不会找到隐藏的重载,而不是它不存在:D
C::f
D::f
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& )
}