4

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 that the problem is that you could assign to a Base* something which is not a Derived*, so we forbid that.

So far, so good.

But, if we apply that principle in depth, why aren't we forbidding such example?

void nasty_function(Base *b)
{
  *b = Base(3); // Ouch!
}

int main(int argc, char **argv)
{
  Derived *d = new Derived;
  nasty_function(d); // Ooops, now *d points to a Base. What would happen now?
}

I agree that nasty_function does something idiotic, so we could say that letting that kind of conversion is fine because we enable interesting designs, but we could say that also for the double-indirection: you got a Base **, but you shouldn't assign anything to its deference because you really don't know where that Base ** comes, just like the Base *.

So, the question: what's special about that extra-level-of-indirection? Maybe the point is that, with just one level of indirection, we could play with virtual operator= to avoid that, while the same machinery isn't available on plain pointers?

Oli4
  • 489
  • 1
  • 9
  • 22
akappa
  • 10,220
  • 3
  • 39
  • 56

6 Answers6

16
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:

Layouts

When we have two levels of indirection it doesn't work because the things being assigned are pointers:

Assigning pointers - type mismatch

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):

Assigning with only one level of indirection

Notice how no pointers are changed here. This is just like any other operation that changes part of an object, like d.y = 42;.

R. Martinho Fernandes
  • 228,013
  • 71
  • 433
  • 510
  • 1
    Yes, I think I should keep that "sub-object" thing in my mental model. BTW, your comment in "new and pointer" is gratuitously harsh, since it is code meant to show a concept. – akappa Jun 29 '12 at 15:50
  • No, I think that's the correct amount of hash. There's way too many Java programmers who write `new` on every line of C++, and that's just silly. – Mooing Duck Jun 29 '12 at 15:58
  • @akappa A proof of concept with modern C++ idioms takes less time to write and is still correct, unlike a proof of concept with ugly, pre-98 ideas. – Etienne de Martel Jun 29 '12 at 16:01
  • But I did it because we were talking about pointer conversions, so making a `Derived *` variables makes that explicit. I could have done a `Derived a; Derived *b = &a`, but I think it is ridiculous to do that just because "here `new` is useless and should be avoided like hell" - I mean, even if we program computers we should retain a bit of flexibility for ourselves. – akappa Jun 29 '12 at 16:01
7

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.

Tilman Vogel
  • 9,337
  • 4
  • 33
  • 32
2

*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.)

Kos
  • 70,399
  • 25
  • 169
  • 233
  • Yep, superficially they look like the same issue but they are two completely different things. Thanks! – akappa Jun 29 '12 at 15:51
0

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.

Antoine
  • 13,494
  • 6
  • 40
  • 52
0
*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;
Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
0

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.

akappa
  • 10,220
  • 3
  • 39
  • 56