views:

218

answers:

5

Suppose Y is a derived class from class X and X declares foo to be virtual. Suppose y is of type (Y*). Then ((X*)y)->foo() will execute the Y version of foo(), but ((X)*y).foo() will execute the X version. Can you tell me why polymorphism does not apply in the dereferenced case? I would expect either syntax would yield the Y version of foo().

A: 

I believe that's simply due to the way the language is specified. References and pointers use late binding wherever possible, while objects use early binding. It would be possible to do late binding in every case (I imagine), but a compiler that did that wouldn't be following the C++ specifications.

Sean Nyman
+8  A: 

A cast always(*) creates a new object of the type you're casting to, which is constructed using the object you're casting.

Casting to X* creates a new pointer (that is, an object of type X*). It has the same value as y, so it still points to the same object, of type Y.

Casting to X creates a new X. It is constructed using *y, but it otherwise has nothing to do with the old object. In your example, foo() is called on this new "temporary" object, not on the object pointed to by y.

You are correct that dynamic polymorphism only applies to pointers and references, not to objects, and this is the reason why: if you have a pointer-to-X then the thing it points to might be a subclass of X. But if you have an X, then it's an X, and nothing else. virtual calls would be pointless.

(*) unless optimisation allows the omission of code that doesn't change the result. But optimisation isn't allowed to change what foo() function is called.

Steve Jessop
Doug
Yes, I should probably say "always creates a new object of the type you're casting to, unless the type you're casting to isn't an object type". Casting to reference type "creates" a new reference bound to the original object.
Steve Jessop
A: 

I think Darth Eru's explanation is correct, and here is why I think C++ behaves that way:

The code (X)*y is like creating a local variable that is of type X. The compiler needs to allocate sizeof(X) space on the stack, and it throws away any extra data included in an object of type Y, so when you call foo() it has to execute the X version. It would be difficult for the compiler to behave in a way that would let you call the Y version.

The code (X*)y is like creating a pointer to an object, and the compiler knows that the object pointed to is X or a subclass of X. At runtime when you dereference the pointer and call foo with "->foo()" the class of the object is determined and the proper function is used.

David Grayson
Remember that (X)*y is defined to be the same as X(*y), i.e. a call to some constructor of X to create a new X object. It wouldn't just be difficult, it's not even desirable for an X object to have the Y version of foo() called on it, just because it happens to have been constructed using a Y object.
Steve Jessop
+2  A: 

The dereferencing (the *y part) is fine, but the cast (the (X) part) creates a new (temporary) object specifically of class X -- that's what casting means. So, the object has to have the virtual table from class X -- consider that the casting will have removed any instance members added by Y in the subclassing (indeed, how could X's copy ctor possibly know about them?), so it would potentially be a disaster if any of Y's overrides were to execute -- secure in their knowledge that this points to an instance of Y, complete with added members and all... when that knowledge was false!

The version in which you cast pointers is of course completely different -- the *X has just the same bits as the Y*, so it's still pointing to a perfectly valid instance of Y (indeed, it's pointing to y, of course).

The sad fact is that, for safety, the copy ctor of a class should really only be called with, as argument, an instance of that class -- not of any subclass; the loss of added instance members &c is just too damaging. But the only way to ensure that is to follow Haahr's excellent advice, "Don't subclass concrete classes"... even though he's writing about Java, the advice is at least as good for C++ (which has this copy ctor "slicing" problem in addition!-)

Alex Martelli
If the loss of Y's extra members is damaging when copy-constructing X, then either class Y does not satisfy the Liskov substitution principle, or more likely the caller hasn't realised that he's casting (unintended slice) and thinks he still has an object of class Y. Either way, I don't think the error is, as such, calling the copy constructor with a Y. It's failing to appreciate that the result is an X constructed from the Y as by any other 1-arg constructor, and not anything polymorphic.
Steve Jessop
The unintended slice is the most likely cause, and if you don't subclass concrete classes (in addition to the other reasons Haahr gives) that's one kind of accident that won't occur!-)
Alex Martelli
Sure, it's a sound rule of thumb. I also agree with Haahr there are cases where it's worth chancing your arm, and subclassing, because sometimes inheritance is actually useful. But it must actually be possible to satisfy Liskov, which is where most subclasses of concrete classes go wrong. If, as Haahr says, *any* aspect of the superclass is "dragged along unintentionally" then you've already failed. But you can subclass concrete classes, in the rare cases it makes sense. If so, the copy ctor makes sense too, and those who understand what slicing is will avoid it anyway, by not doing it.
Steve Jessop
+9  A: 

You are slicing the Y object part and copy the object into an X object. The function then called is called on an X object, and thus the function of X is called.

When you specify a type in C++ in a declaration or cast, that is meant to say that the object declared or casted-to is actually of that type, not of a derived type.

If you want to merely treat the object is being of type X (that is to say, if you want the static type of the expression be X, but still want it to denote an Y object) then you cast to a reference type

((X&)*y).foo()

This will call the function in the Y object, and will not slice nor copy into an X object. In steps, this does

  • Dereference the pointer y, which is of type Y*. Dereferencing yields an lvalue expression of type Y. An lvalue expression can actually denote an object of a derived type, even if its static type is the one of its base.
  • Cast to a X&, which is a reference to X. That will yield an lvalue expression of type X.
  • Call the function.

Your original cast did

  • Dereference the pointer y.
  • The resulting expression casted to X. This will yield to a copy operation into a new X object. The resulting expression of that is an rvalue expression of static type X. The dynamic type of the object denoted is also X, as is with all rvalue expressions.
  • Call the function.
Johannes Schaub - litb