27

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?
}
4

6 回答 6

24

关于纯粹的逆变问题

为语言添加逆变器会带来很多潜在的问题或不干净的解决方案,并且几乎没有优势,因为它可以在没有语言支持的情况下轻松模拟:

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每个对象来说,相同的对象的有效参数。现在,通过向层次结构添加一个额外的级别,我们最终会遇到设计问题:是有效的覆盖还是应该是?oP::fQ::fR::f(B&)P::fR::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++ 中,在第一个示例中(使用AB和)删除手动调度 [0],并且C是不同的签名而不是覆盖。在这两种情况下,它们实际上都是相同函数名的重载,只是由于 C++ 查找规则,重载将被隐藏。但这仅意味着编译器默认不会找到隐藏的重载,而不是它不存在:DC::fD::fC::fD::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& )
}
于 2010-06-09T23:03:06.427 回答
16
class A {
    public void f(String s) {...}
    public void f(Integer i) {...}
}

class B extends A {
    public void f(Object o) {...} // Which A.f should this override?
}
于 2010-06-08T11:38:00.017 回答
5

对于 C++,Stroustrup 在The Design & Evolution of C++的第 3.5.3 节中简要讨论了隐藏的原因。他的推理是(我解释一下)其他解决方案会引发同样多的问题,自 C With Classes 时代以来就是这样。

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

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

这目前是一个错误,因为 B 的 copy() 隐藏了 A。如果不是错误,则只有 B 的 A 部分可以由 A 的 copy() 函数更新。

再一次,我已经转述了-如果您有兴趣,请阅读这本书,非常好。

于 2010-06-08T09:08:59.057 回答
3

尽管这在任何 oo 语言中都是一个不错的选择,但我仍然需要在我目前的工作中遇到它的适用性。

也许真的不需要它。

于 2010-06-08T09:11:58.010 回答
2

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

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

于 2010-06-08T12:12:21.163 回答
1

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

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

class A {
    public void f(String s) {...}
}

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

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

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

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

编辑:从那以后,我发现这个 C++ FQA 条目(20.8)与上述一致 - 重载的存在给参数逆变带来了严重的问题。

于 2010-06-10T10:32:42.590 回答