关于逆变性的问题
在语言中添加逆变性会带来许多潜在问题或不太干净的解决方案,并且提供的优势很小,因为它可以在没有语言支持的情况下轻松模拟:
struct A {};
struct B : A {};
struct C {
virtual void f( B& );
};
struct D : C {
virtual void f( A& );
virtual void f( B& b ) {
D::f( static_cast<A&>(b) );
}
};
通过简单的额外跳转,您可以手动克服不支持逆变的语言的问题。在此示例中,f( A& )
无需为虚函数,且调用是完全合格的,以抑制虚分派机制。
这种方法展示了在将逆变添加到没有完全动态分派的语言中时出现的第一个问题之一:
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
的有效参数。但是,通过在层级结构中添加额外的层级,我们面临着设计问题: 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 );
p.f( b );
}
在[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 );
}
如果你检查一下
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.C::f( b );
}
通过轻微改变类的定义,它可以被制作成与Java完全相同的工作方式:
struct D : C {
using C::f;
virtual void f( A& );
};
int main() {
D d; B b;
d.f( b );
}