Adam Stelmaszczyk
Adam Stelmaszczyk

Reputation: 19837

Multiple inheritance and polymorphism questions

Consider this C++ code:

#include <iostream>
using namespace std;

struct B {
    virtual int f() { return 1; }
    int g() { return 2; }
};
struct D1 : public B { // (*)
    int g() { return 3; }
};
struct D2 : public B { // (*)
    virtual int f() { return 4; }
};
struct M : public D1, public D2 {
    int g() { return 5; }
};

int main() {
    M m;
    D1* d1 = &m;
    cout << d1->f()
         << static_cast<D2&>(m).g()
         << static_cast<B*>(d1)->g()
         << m.g();
}

It prints 1225. If we make virtual inheritance, i.e. add virtual before public in lines marked with (*), it prints 4225.

  1. Can you explain why 1 changes to 4?
  2. Can you explain meaning of static_cast<D2&>(m) and static_cast<B*>(d1)?
  3. How you are you not getting lost in this kind of combinations? Are you drawing something?
  4. Is it common to spot such complex settings in normal projects?

Upvotes: 11

Views: 823

Answers (5)

WhozCraig
WhozCraig

Reputation: 66194

Pictures speak louder than words, so before the answers...


Class M hierarchy WITHOUT virtual base inheritance of B for D1 and D2:

    M
   / \
  D1 D2
  |   |
  B   B

Class M hierarchy WITH virtual base inheritance of B for D1 and D2:

    M
   / \
  D1 D2
   \ /
    B

  1. Cross-Delegation, or as I like to call it, sibling-polymorphism with a twist. The virtual base inheritance will fix up the B::f() override to be D2:f(). Hopefully the picture helps explain this when you consider where the virtual functions are implemented, and what they override as a result of the inheritance chains.

  2. the static_cast operator usage in this case drives conversion from derived-to-base class types.

  3. Lots of experience reading really bad code and knowing how the underpinnings of the language 'work'

  4. Thankfully no. It is not common. The original iostream libraries would have given you nightmares, though, if this is at-all confusing.

Upvotes: 5

Peter Alexander
Peter Alexander

Reputation: 54270

Can you explain why 1 changes to 4?

Why does it change to 4? Because of cross-delegation.

Here's the inheritance graph before virtual inheritance:

B   B
|   |
D1  D2
 \ /
  M

d1 is a D1, so it has no idea that D2 even exists, and its parent (B) has no idea that D2 exists. The only possible result is that B::f() is called.

After virtual inheritance is added, the base classes are merged together.

  B
 / \
D1  D2
 \ /
  M

Here, when you ask d1 for f(), it looks to its parent. Now, they share the same B, so B's f() will be overridden by D2::f() and you get 4.

Yes, this is weird because it means that D1 has managed to call a function from D2, which is knows nothing about. This is one of the more odd parts of C++ and it is generally avoided.


Can you explain meaning of static_cast(m) and static_cast(d1)?

What don't you understand? They cast m and d1 to D2& and B* respectively.


How you are you not getting lost in this kind of combinations? Are you drawing something?

Not in this case. It's complicated, but small enough to keep in your head. I've drawn the graph in the above example to make things as clear as possible.


Is it common to spot such complex settings in normal projects?

No. Everyone knows to avoid the dreaded diamond pattern of inheritance because it's simply too complicated, and there's usually a simpler way to do whatever you want to do.

In general, it's better to prefer composition over inheritance.

Upvotes: 4

Branko Dimitrijevic
Branko Dimitrijevic

Reputation: 52107

1) Can you explain why 1 changes to 4?

Without virtual inheritance, there are two instances of B in M, one for each branch of this "diamond". One of the diamond edges (D2) overrides the function and the other (D1) doesn't. Since d1 is declared as D1, d1->f() means you wish to access the copy of B whose function was not overridden. If you were to cast to D2, you'd get a different result.

By using virtual inheritance, you merge the two instances of B into one, so D2::f effectively overrides B:f once the M is made.

2) Can you explain meaning of static_cast<D2&>(m) and static_cast<B*>(d1)?

They cast to D2& and B* respectively. Since g is not virtual, the B:::g gets called.

3) How you are you not getting lost in this kind of combinations? Are you drawing something?

Sometimes ;)

4) Is it common to spot such complex settings in normal projects?

Not too common. In fact there are entire languages that get by just fine without multiple let alone virtual inheritance at all (Java, C#...).

However, there are occasions where it can make things easier, especially in library development.

Upvotes: 2

iammilind
iammilind

Reputation: 69988

(1) Can you explain why 1 changes to 4?

Without virtual inheritance, there are 2 independent hierarchies of inheritance; B->D1->M and B->D2->M. So imagine 2 virtual function tables (though this is implementation defined).
When you invoke f() with D1*, it just knows about B::f() and that's it. With virtual inheritance, base class B is delegated to M and thus D2::f() is considered as part of class M.

(2) Can you explain meaning of static_cast<D2&>(m) and static_cast<B*>(d1)?

static_cast<D2&>(m), is like considering object of class M as class D2
static_cast<B*>(d1), is like considering pointer of class D1 as class B1.
Both are valid casts.
Since g() is not virtual the function choice happens at compile-time. Had it been virtual then all these casting won't matter.

(3) How you are you not getting lost in this kind of combinations? Are you drawing something?

Ofcourse it's complex and at first glance if there are so many of such classes, one might get easily lost.

(4) Is it common to spot such complex settings in normal projects?

Not at all, it's unusual and sometimes a code smell.

Upvotes: 2

Dietmar K&#252;hl
Dietmar K&#252;hl

Reputation: 153840

This question is actually multiple questions:

  1. Why is the virtual function B::f() not overridden when non-virtual inheritance is used? The answer is, of course, that you have two Base objects: one as the base of D1 which overrides f() and one as the base of D2 which doesn't override f(). Depending from which branch you consider your object to derive when calling f(), you'll get different results. When you change the setup to have just one B subobject, any override within the inheritance graph is considered (and if both branches override it, I think you'll get an error unless you override it in a place where the branches are merged again.
  2. What does static_cast<D2&>(m) mean? Since there are two versions of f() coming from the Base, you need to choose which one you want. With static_cast<D2&>(m) you view the M as a D2 object. Without the cast the compiler won't be able to tell which one of the two subjects you are looking at and it would give an ambiguity error.
  3. What does static_cast<B*>(d1) mean? It happens to be unnecessary but views the object as a B* object only.

Generally, I tend to avoid multiple inheritance for anything which isn't trivial. Most of the time I' using multiple inheritance to take advantage of the empty base optimization or to create something with a variable number of members (think std::tuple<...>). I'm not sure if I ever came across an actual need to use multiple inheritance to deal with polymorphism in production code.

Upvotes: 2

Related Questions