C++多态性:((X*)y)->foo()与((X)*y).foo()的区别

5
假设Y是从类X派生的类,X声明foo为虚函数。假设y是(Y*)类型。那么((X*)y)->foo()将执行foo()的Y版本,但((X)*y).foo()将执行X版本。你能告诉我为什么在解引用的情况下多态不适用吗?我希望任何一种语法都会产生foo()的Y版本。
5个回答

10
你正在对Y对象进行切片,并将该对象的一部分复制到X对象中。然后在X对象上调用函数,因此调用了X的函数。
当你在C++中声明或转换类型时,这意味着声明的对象或转换后的对象实际上是该类型,而不是派生类型。
如果你想仅把对象视为X类型(也就是说,如果你希望表达式的静态类型为X,但仍希望它表示一个Y对象),那么你需要将其强制转换为引用类型。
((X&)*y).foo()

这将调用Y对象中的函数,不会切片或复制到一个X对象中。具体而言:

  • 解引用指针y,它是类型为Y*的指针。解引用将得到一个lvalue表达式,类型为Y。即使其静态类型为其基类类型,lvalue表达式实际上可以表示派生类型的对象。
  • 进行X&类型转换,它是X的引用。这将产生一个lvalue表达式,类型为X
  • 调用函数。

您原来的强制类型转换执行了以下操作:

  • 解引用指针y
  • 所得到的表达式强制转换为X。这将导致将数据复制到一个新的X对象中。该表达式的结果是一个静态类型为Xrvalue表达式。所指对象的动态类型也是X,这与所有rvalue表达式相同。
  • 调用函数。

8

强制类型转换总是创建一个新的对象,该对象的类型是您要进行转换的类型,并且使用您要进行转换的对象进行构造。

将其转换为X*会创建一个新的指针(即,类型为X*的对象)。它具有与y相同的值,因此仍然指向类型为Y的同一对象。

将其转换为X会创建一个新的X。它使用*y进行构造,但除此之外与旧对象无关。在您的示例中,foo()在这个新的“临时”对象上调用,而不是在y指向的对象上调用。

您正确地指出动态多态性仅适用于指针和引用,而不适用于对象,原因是:如果您有一个指向X的指针,则它指向的东西可能是X的子类。但是,如果您有一个X,则它是一个X,没有别的了。虚拟调用毫无意义。

(*)除非优化允许省略不改变结果的代码。但是,优化不允许更改调用foo()函数的内容。


1
...并且((X&)*y).foo()确实调用了派生类的foo,因为将Y&强制转换为X&不会创建新的X。 - Doug
是的,我应该说“除非您要转换的类型不是对象类型,否则它总是会创建一个新的要转换的类型的对象”。将引用类型强制转换会创建一个新的引用,绑定到原始对象。 - Steve Jessop

2
对于解引用部分(*y),是没有问题的,但强制类型转换((X))创建了一个新的(临时)对象,特别是属于类X——这就是强制类型转换的含义。因此,该对象必须具有来自类X的虚表——需要考虑到转换将删除子类中添加的任何实例成员(事实上,X的复制构造函数怎么可能知道它们?),所以如果Y的任何重写代码执行,那么就有可能导致灾难——因为他们确信this指向Y的实例,包括所有添加的成员等等...当这种情况是错误的!

当然,使用指针进行强制类型转换的版本完全不同——*XY*具有完全相同的位,因此仍然指向完全有效的Y实例(当然,它指向y)。

悲哀的事实是,为了安全起见,类的复制构造函数应该只使用该类的实例作为参数调用,而不是任何子类的实例;失去附加实例成员等太过严重。但确保这一点的唯一方法是遵循Haahr的卓越建议,“不要对具体类进行子类化”……即使他写的是Java,这个建议对于C++同样适用(此外,C++还存在这种复制构造函数“切片”问题!-)

如果在复制构造X时,丢失Y的额外成员会导致损坏,则类Y要么不符合Liskov替换原则,要么更可能是调用者没有意识到他正在进行强制转换(意外切片),并认为他仍然拥有类Y的对象。无论哪种方式,我认为错误不在于使用Y调用复制构造函数,而是未能意识到结果是由任何其他1个参数构造函数从Y构建的X,并不是任何多态的东西。 - Steve Jessop
非预期的切片是最有可能的原因,如果你不对具体类进行子类化(除了 Haahr 给出的其他原因),那么这种事故就不会发生!-) - Alex Martelli
当然,这是一个很好的经验法则。我也同意Haahr的观点,有些情况下值得冒险和子类化,因为有时继承确实很有用。但必须确实能够满足Liskov原则,这就是大多数具体类的子类出错的地方。如果像Haahr所说的那样,超类的“任何”方面都被“无意中拖累”,那么你已经失败了。但在极少数情况下,您可以对具体类进行子类化。如果是这样,复制构造函数也是有意义的,而那些理解什么是切片的人会通过不这样做来避免它。 - Steve Jessop

0

我相信这只是语言规范的方式所致。在可能的情况下,引用和指针使用后期绑定,而对象使用早期绑定。在每种情况下都采用后期绑定是可能的(我想象中),但这样做的编译器将不符合C++规范。


0

我认为 Darth Eru 的解释是正确的,这就是我认为 C++ 行为如此的原因:

代码 (X)*y 就像创建了一个类型为 X 的本地变量。编译器需要在堆栈上分配 sizeof(X) 的空间,并且会丢弃包含在类型为 Y 的对象中的任何额外数据,所以当调用 foo() 时,它必须执行 X 版本。编译器要让你调用 Y 版本的行为可能很困难。

代码 (X*)y 就像创建了一个指向对象的指针,编译器知道指向的对象是 X 或 X 的子类。运行时,当您解引用指针并使用 "->foo()" 调用 foo 时,对象的类别被确定并使用正确的函数。


请记住,(X)*y 的定义与 X(*y) 相同,即调用 X 的某个构造函数来创建一个新的 X 对象。让一个 X 对象调用 Y 版本的 foo() 并不仅仅是困难的,而且也不可取,因为它恰好是使用 Y 对象构建的。 - Steve Jessop

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接