为什么覆盖时没有参数逆变?

2024-04-02

C++ 和 Java 在重写方法时支持返回类型协变。

然而,两者都不支持参数类型的逆变 - 相反,它会转化为重载(Java)或隐藏(C++)。

这是为什么?在我看来,允许这样做并没有什么坏处。我can在 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?
}

关于纯粹的逆变问题

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

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& )
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么覆盖时没有参数逆变? 的相关文章

随机推荐