Reputation: 19837
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
changes to 4
?static_cast<D2&>(m)
and static_cast<B*>(d1)
?Upvotes: 11
Views: 823
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
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.
the static_cast
operator usage in this case drives conversion from derived-to-base class types.
Lots of experience reading really bad code and knowing how the underpinnings of the language 'work'
Thankfully no. It is not common. The original iostream libraries would have given you nightmares, though, if this is at-all confusing.
Upvotes: 5
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
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)
andstatic_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
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)
andstatic_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
Reputation: 153840
This question is actually multiple questions:
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.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.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