Ok, I was reading through this entry in the FQA dealing about the issue of converting a Derived**
to a Base**
and why it is forbidden, and I got th
nasty_function(d); // Ooops, now *d points to a Base. What would happen now?
No, it doesn't. It points to a Derived
. The function simply changed the Base
subobject in the existing Derived
object. Consider:
#include <cassert>
struct Base {
Base(int x) : x(x) {}
int x;
};
struct Derived : Base {
Derived(int x, int y) : Base(x), y(y) {}
int y;
};
int main(int argc, char **argv)
{
Derived d(1,2); // seriously, WTF is it with people and new?
// You don't need new to use pointers
// Stop it already
assert(d.x == 1);
assert(d.y == 2);
nasty_function(&d);
assert(d.x == 3);
assert(d.y == 2);
}
d
doesn't magically become a Base
, does it? It's still a Derived
, but the Base
part of it changed.
In pictures :)
This is what Base
and Derived
objects look like:
When we have two levels of indirection it doesn't work because the things being assigned are pointers:
Notice how neither of the Base
or Derived
objects in question are attempted to be changed: only the middle pointer is.
But, when you only have one level of indirection, the code modifies the object itself, in a way that the object allows (it can forbid it by making private, hiding, or deleting the assignment operator from a Base
):
Notice how no pointers are changed here. This is just like any other operation that changes part of an object, like d.y = 42;
.
*b = Base(3)
calls Base::operator=(const Base&)
, which is actually present in Derived
as member functions (inc. operators) are inherited.
What would happen then (calling Derived::operator=(const Base&)
) is sometimes called "slicing", and yes, it's bad (usually). It's a sad consequence of the sheer omnipresence of the "become-like" operator (the =
) in C++.
(Note that the "become-like" operator doesn't exist in most OO languages like Java, C# or Python; =
in object contexts there means reference assignment, which is like pointer assignment in C++;).
Summing up:
Casts Derived**
-> Base**
are forbidden, because they can cause a type error, because then you could end up with a pointer of type Derived*
pointing to an object of type Base
.
The problem you mentioned isn't a type error; it's a different type of error: mis-use of the interface of the derived
object, rooting from the sorry fact that it has inherited the "become-like" operator of its parent class.
(Yes, I call op= in objects contexts "become-like" deliberately, as I feel that "assignment" isn't a good name to show what's happening here.)
Reading through the good answers of my question I think I got the point of the issue, which comes from first principles in OO and has nothing to do with sub-objects and operator overloading.
The point is that you can use a Derived
whenever a Base
is required (substitution principle), but you cannot use a Derived*
whenever a Base*
is needed due to the possibility of assigning pointers of instances of derived classes to it.
Take a function with this prototype:
void f(Base **b)
f
can do a bunch of things with b
, dereferencing it among other things:
void f(Base **b)
{
Base *pb = *b;
...
}
If we passed to f
a Derived**
, it means that we are using a Derived*
as a Base*
, which is incorrect since we may assign a OtherDerived*
to Base*
but not to Derived*
.
On the other way, take this function:
void f(Base *b)
If f
dereferences b
, then we would use a Derived
in place of a Base
, which is entirerly fine (provided that you give a correct implementation of your class hierarchies):
void f(Base *b)
{
Base pb = *b; // *b is a Derived? No problem!
}
To say it in another way: the substitution principles (use a derived class instead of the base one) works on instances, not on pointers, because the "concept" of a pointer to A
is "points to an instance of whichever class inherits A
", and the set of classes which inherits Base
strictly contains the set of classes that inherits Derived
.
No, nasty_function()
isn't as nasty as it sounds. As the pointer b
points to something that is-a Base
, it's perfectly legal to assign a Base
-value to it.
Take care: your "Ooops" comment is not correct: d
still points to the same Derived
as before the call! Only, the Base
part of it was reassigned (by value!). If that gets your whole Derived
out of consistency, you need to redesign by making Base::operator=()
virtual. Then, in the nasty_function()
, in fact the Derived
assignment operator will be called (if defined).
So, I think, your example does not have that much to do with the pointer-to-pointer case.
*b = Base(3); // Ouch!
Here the object at *b
really is a B
, it's the base sub-object of *d
. Only that base sub-object gets modified, the rest of the derived object isn't changed and d
still points to the same object of the derived type.
You might not want to allow the base to be modified, but in terms if the type system it's correct. A Derived
is a Base
.
That's not true for the illegal pointer case. A Derived*
is convertible to Base*
but is not the same type. It violates the type system.
Allowing the conversion you're asking about would be no different to this:
Derived* d;
Base b;
d = &b;
d->x;
Well the code you gave makes sense. Indeed the assignement operator cannot overrite data specific to Derived but only base. Virtual functions are still from Derived and not from Base.