关于纯粹的逆变问题
在语言中添加逆变会带来很多潜在的问题或不干净的解决方案,并且提供的优势很小,因为它可以在没有语言支持的情况下轻松模拟:
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
,同一个对象is一个有效的论据Q::f
。现在,通过向层次结构添加额外的级别,我们最终会遇到设计问题:是R::f(B&)
有效覆盖P::f
或者应该是R::f(A&)
?
无逆变性R::f( B& )
显然是覆盖P::f
,因为签名是完美匹配的。一旦你将逆变添加到中间级别,问题就在于有些参数在中间级别是有效的。Q
水平,但两者都不在P
or 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
. Since r
是本地对象而不是引用或指针,没有动态调度机制,最佳匹配是R::f( B& )
。同时,在[2]中,通过对基类的引用进行调用,并且虚拟调度机制启动。
Since R::f( A& )
是覆盖Q::f( A& )
这又是覆盖P::f( B& )
,编译器应该调用R::f( A& )
。虽然这可以在语言中完美定义,但可能会令人惊讶地发现两个几乎完全相同的调用 [1] 和 [2] 实际上调用了不同的方法,并且在 [2] 中系统将调用一个not best参数的匹配。
当然,也可以有不同的说法: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
and D
) 删除手动调度 [0],C::f
and D::f
是不同的签名而不是覆盖。在这两种情况下,它们实际上是相同函数名的重载,但由于 C++ 查找规则的原因,略有不同C::f
过载将被隐藏D::f
。但这仅仅意味着编译器不会找到hidden默认情况下重载,并不是说它不存在:
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& )
}