为什么没有用于覆盖的参数反方差?

2022-09-01 15:27:35

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

答案 1

关于反方差的纯粹问题

向语言添加逆方差会打开很多潜在的问题或不透明的解决方案,并且几乎没有什么优势,因为它可以在没有语言支持的情况下轻松模拟:

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::fP::foP::fQ::fR::f(B&)P::fR::f(A&)

没有逆变显然是 的覆盖,因为签名是一个完美匹配。一旦你向中间级别添加逆变,问题就在于有些参数在该级别有效,但在两个级别上都无效。为了满足要求,唯一的选择是强制签名为 ,以便可以编译以下代码:R::f( B& )P::fQPRRQR::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] 中,有一个对 的成员方法的直接调用。由于 是 本地 对象 而不是 引用 或 指针, 因此 没有 动态 调度 机制, 最佳匹配是 。同时,在[2]中,通过对基类的引用进行调用,并且虚拟调度机制启动。RrR::f( B& )

因为 是 的覆盖,而它的覆盖又是 的覆盖,编译器应该调用 。虽然这在语言中可以完美地定义,但可能会惊讶地发现,两个几乎完全相同的调用[1]和[2]实际上调用了不同的方法,并且在[2]中,系统会调用参数的不匹配R::f( A& )Q::f( A& )P::f( B& )R::f( A& )

当然,可以有不同的论点:应该是正确的覆盖,而不是。在这种情况下的问题是: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?
}

如果检查该类,则前面的代码是完全正确的:采用 as 参数。编译器没有理由抱怨该代码。但问题是,在最后一个假设下,需要一个而不是一个作为论据!实际的重写将无法处理参数,即使调用位置的方法签名看起来完全正确。这条路径引导我们确定第二条路径比第一条路径差得多。 不可能是 的覆盖。QQ::fA&R::fB&A&aR::f( B& )Q::f( A& )

遵循最小意外原则,对于编译器实现者和程序员来说,在函数参数中不具有相反的方差要简单得多。不是因为它不可行,而是因为代码中会有怪癖和惊喜,并且考虑到如果该功能在语言中不存在,则存在简单的解决方法。

关于重载与隐藏

在 Java 和 C++中,在第一个示例(带有 、 和 )中,删除了手动调度 [0],并且是不同的签名,而不是覆盖。在这两种情况下,它们实际上是相同函数名称的重载,略有不同,由于C++查找规则,重载将被 隐藏。但这只意味着编译器在默认情况下不会找到隐藏的重载,而不是它不存在:ABCDC::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& )
}

答案 2
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?
}