为什么没有参数反差方法可以覆盖?

Oak*_*Oak 26 c++ java oop overriding variance

重写方法时,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?
}
Run Code Online (Sandbox Code Playgroud)

Dav*_*eas 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) );
   }
};
Run Code Online (Sandbox Code Playgroud)

通过简单的额外跳转,您可以手动克服不支持反差的语言问题.在示例中,f( A& )不需要是虚拟的,并且调用完全限定以禁止虚拟调度机制.

这种方法显示了在对没有完全动态调度的语言添加逆方差时出现的首要问题之一:

// assuming that contravariance was supported:
struct P {
   virtual f( B& ); 
};
struct Q : P {
   virtual f( A& );
};
struct R : Q {
   virtual f( ??? & );
};
Run Code Online (Sandbox Code Playgroud)

有效的逆转,Q::f将是一个覆盖P::f,并且对于可以作为o参数的每个对象都可以P::f,同一个对象一个有效的参数Q::f.现在,通过在层次结构中添加额外的级别,我们最终会R::f(B&)遇到设计问题:是有效的覆盖P::f还是应该是R::f(A&)

没有逆变R::f( B& )显然是一种超越P::f,因为签名是完美的匹配.一旦你添加到逆变中级的问题是,有观点认为是在有效的Q水平,但不是在任PR水平.为了R满足Q要求,唯一的选择是强制签名R::f( A& ),以便以下代码可以编译:

int main() {
   A a; R r;
   Q & q = r;
   q.f(a);
}
Run Code Online (Sandbox Code Playgroud)

同时,语言中没有任何内容禁止以下代码:

struct R : Q {
   void f( B& );    // override of Q::f, which is an override of P::f
   virtual f( A& ); // I can add this
};
Run Code Online (Sandbox Code Playgroud)

现在我们有一个有趣的效果:

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& )
}
Run Code Online (Sandbox Code Playgroud)

在[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?
}
Run Code Online (Sandbox Code Playgroud)

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

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

关于重载与隐藏

无论是在Java和C++,在第一实施例(具有A,B,CD)去除所述手动调度[0],C::f并且D::f是不同的签名和未覆盖.在这两种情况下,它们实际上都是相同函数名的重载,但由于C++查找规则,C::f重载将隐藏起来D::f.但这只意味着编译器默认情况下不会找到隐藏的重载,而不是它不存在:

int main() {
   D d; B b;
   d.f( b );    // D::f( A& )
   d.C::f( b ); // C::f( B& )
}
Run Code Online (Sandbox Code Playgroud)

在类定义稍有变化的情况下,可以使其与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& )
}
Run Code Online (Sandbox Code Playgroud)

  • 这是我能想到的第一个.如果您阅读C++的设计和演变,您会发现所有语言设计决策归结为:它提供了哪些优势,它创建了哪些问题,是否可以使用可用的语言设施实现?该委员会相当保守,如果某个功能引发任何问题,除非它提供了很大的优势,并且无法使用当前语言实现,否则该功能将被丢弃. (2认同)

Don*_*oby 15

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?
}
Run Code Online (Sandbox Code Playgroud)

  • 好点,虽然我看不出它无法覆盖两者,因为每个`f`调用都可以合法地重定向到`Bf`. (8认同)

小智 5

对于C++,Stroustrup在C++ 的设计和演变的第3.5.3节中简要讨论了隐藏的原因.他的推理(我解释)其他解决方案引发了同样多的问题,而且自从C With Classes天以来就是这样.

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

A a;
B b;
b.copy( & a );
Run Code Online (Sandbox Code Playgroud)

这是一个错误,因为B的副本()隐藏了A的.如果它不是错误,只有B的A部分可以通过A的copy()函数更新.

再一次,我已经解释了 - 如果你有兴趣,请阅读这本书,这本书很棒.

  • 这听起来不像是对我不利.如果我们有`void A :: copy(A*)`和`void B :: copy(B*)`,如果我们有'B <:A`,那么如果我们有'void B,那么逆变就是这样: :copy(A*)`和`void A :: copy(B*)`.您有协方差,这对于返回类型是允许的,但对于参数类型是类型不安全的.(虽然我可能已经倒退了 - 我倾向于翻转它们.但我很确定.) (4认同)