How can I avoid the Diamond of Death when using multiple inheritance?
Asked Answered
F

9

71

http://en.wikipedia.org/wiki/Diamond_problem

I know what it means, but what steps can I take to avoid it?

Friedafriedberg answered 26/9, 2008 at 1:36 Comment(5)
I want to say "don't use multiple inheritance," but that's just being a cad. I'd love to see a good answer to this, too.Os
"Diamond of Death" is a bit dramatic. What exactly do you want to know.Mcclimans
It's widely known as the Deadly Diamond of Death. Google it.Friedafriedberg
Google tells me it'susually known as the "Diamond Problem", except in the Java community, where the more drastic term is used to justify why Java "solves" the problem by disallowing it.Valiant
There is no "death" here. Both virtual and "standard" inheritance have their use (very rarely, though).Lest
B
82

A practical example:

class A {};
class B : public A {};
class C : public A {};
class D : public B, public C {};

Notice how class D inherits from both B & C. But both B & C inherit from A. That will result in 2 copies of the class A being included in the vtable.

To solve this, we need virtual inheritance. It's class A that needs to be virtually inherited. So, this will fix the issue:

class A {};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
Byebye answered 26/9, 2008 at 12:57 Comment(4)
That's only avoiding A being twice in memory, it does not avoid any problems caused by the Diamond. See tinyurl.com/abtjcb ; how do you implement getDepartment, that it always returns the right thing? You can't! Your design is flawed. See tinyurl.com/ccjnk6Doddered
That's what scope is for. Alternatively you can use the "using" statement in class D.Byebye
Isn't this answer only valid for classes you have control over? If B and C are in a library provided by someone else, or they're part of a code base you cannot change this 'solution' doesn't work at all. It also goes against the whole principle of OOP that a base class shouldn't be concerned with the derived classes, but here B and C suddenly have to change because of some class D that was added later in the day.Widget
Doesn't work for me. Got linker "vtable" error. See my "answer" post below.Fussbudget
Y
16

I'd stick to using multiple inheritance of interfaces only. While multiple inheritance of classes is attractive sometimes, it can also be confusing and painful if you rely on it regularly.

Yuille answered 26/9, 2008 at 1:49 Comment(3)
@Ibnrushd C++ does have interfaces, although it is not a language construct as for example found in Java. Instead, they are just pure virtual base classes.Deannedeans
@jlh: I'll conceed that point, although I'd contend that while C++ itself doesn't have interfaces, the language does allow you to implement them.Ibnrushd
I never met somebody who did not understand that it would mean inheriting pure abstract classes in this contextEthelyn
E
14

virtual inheritance. That's what it's there for.

Embracery answered 26/9, 2008 at 1:39 Comment(3)
Where in the inheritance hierarchy?Friedafriedberg
If you have B and C derived from A, and D derived from B and C, then B and C must both declare A as a virtual base. Specifically, each instance of virtual inheritance of the same class is collapsed into one class. Any non-virtual ones will not be collapsed, causing the diamond to recur.Cipolin
While virtual inheritence is the feature for getting around the Diamond of Death problem, I think that there are better ways to work around the problem. Namely, inheriting from abstract base classes (interface classes) instead of inheriting from multiple concrete classes.Debrahdebrecen
C
6

Inheritance is a strong, strong weapon. Use it only when you really need it. In the past, diamond inheritance was a sign that I was going to far with classification, saying that a user is an "employee" but they are also a "widget listener", but also a ...

In these cases, it's easy to hit multiple inheritance issues.

I solved them by using composition and pointers back to the owner:

Before:

class Employee : public WidgetListener, public LectureAttendee
{
public:
     Employee(int x, int y)
         WidgetListener(x), LectureAttendee(y)
     {}
};

After:

class Employee
{
public:
     Employee(int x, int y)
         : listener(this, x), attendee(this, y)
     {}

     WidgetListener listener;
     LectureAttendee attendee;
};

Yes, access rights are different, but if you can get away with such an approach, without duplicating code, it's better because it's less powerful. (You can save the power for when you have no alternative.)

Coupling answered 27/9, 2008 at 6:9 Comment(2)
And what you did is that you multiplied your memory usage by a lot. No thanks.Sari
Composition vs Inheritance. Fight!Winegar
P
3
class A {}; 
class B : public A {}; 
class C : public A {}; 
class D : public B, public C {};

In this the attributes of Class A repeated twice in Class D which makes more memory usage... So to save memory we make a virtual attribute for all inherited attributes of class A which are stored in a Vtable.

Panter answered 15/7, 2012 at 19:7 Comment(0)
C
2

Well, the great thing about the Dreaded Diamond is that it's an error when it occurs. The best way to avoid is to figure out your inheritance structure beforehand. For instance, one project I work on has Viewers and Editors. Editors are logical subclasses of Viewers, but since all Viewers are subclasses - TextViewer, ImageViewer, etc., Editor does not derive from Viewer, thus allowing the final TextEditor, ImageEditor classes to avoid the diamond.

In cases where the diamond is not avoidable, using virtual inheritance. The biggest caveat, however, with virtual bases, is that the constructor for the virtual base must be called by the most derived class, meaning that a class that derives virtually has no control over the constructor parameters. Also, the presence of a virtual base tends to incur a performance/space penalty on casting through the chain, though I don't believe there is much of a penalty for more beyond the first.

Plus, you can always use the diamond if you are explicit about which base you want to use. Sometimes it's the only way.

Cipolin answered 26/9, 2008 at 2:1 Comment(0)
M
1

I would suggest a better class design. I'm sure there are some problems that are solved best through multiple inheritance, but check to see if there is another way first.

If not, use virtual functions/interfaces.

Malignant answered 26/9, 2008 at 12:50 Comment(1)
"check to see if there is another way first" Why?Eschalot
H
0

Use inheritance by delegation. Then both classes will point to a base A, but have to implement methods that redirect to A. It has the side effect of turning protected members of A into "private" members in B,C, and D, but now you don't need virtual, and you don't have a diamond.

Hidalgo answered 11/2, 2011 at 23:24 Comment(0)
U
0

This is all I have in my notes about this topic. I think this would help you.

The diamond problem is an ambiguity that arises when two classes B and C inherit from A, and class D inherits from both B and C. If there is a member in A that B and C, and D does not override it, then which member does D inherit: that of B, or that of C?

struct A { int a; };
struct B : A { int b; };
struct C : A { int c; };
struct D : B, C {};

D d;
d.a = 10;       //error: ambiguous request for 'a'

In the above example, both B & C inherit A, and they both have a single copy of A. However D inherits both B & C, therefore D has two copies of A, one from B and another from C. If we need to access the data member an of A through the object of D, we must specify the path from which the member will be accessed: whether it is from B or C because most compilers can’t differentiate between two copies of A in D.

There are 4 ways to avoid this ambiguity:

1- Using the scope resolution operator we can manually specify the path from which a data member will be accessed, but note that, still there are two copies (two separate subjects) of A in D, so there is still a problem.

d.B::a = 10; // OK
d.C::a = 100; // OK
d.A::a = 20; // ambiguous: which path the compiler has to take D::B::A or D::C::A to initialize A::a

2- Using static_cast we can specify which path the compiler can take to reach to data member, but note that, still there are two copies (two separate suobjects) of A in D, so there is still a problem.

static_cast<B&>(static_cast<D&>(d)).a = 10;
static_cast<C&>(static_cast<D&>(d)).a = 100;
d.A::a = 20; // ambiguous: which path the compiler has to take D::B::A or D::C::A to initialize A::a

3- Using overridden, the ambiguous class can overriden the member, but note that, still there are two copies (two separate suobjects) of A in D, so there is still a problem.

struct A { int a; };
struct B : A { int b; };
struct C : A { int c; };
struct D : B, C { int a; };
 
D d;
d.a = 10;    // OK: D::a = 10
d.A::a = 20; // ambiguous: which path the compiler has to take D::B::A or D::C::A to initialize A::a

3- Using virtual inheritance, the problem is completely solved: If the inheritance from A to B and the inheritance from A to C are both marked "virtual", C++ takes special care to create only one A subobject,

struct A { int a; };
struct B : virtual A { int b; };
struct C : virtual A { int c; };
struct D : B, C {};
 
D d;
d.a = 10;    // OK: D has only one copy of A - D::a = 10
d.A::a = 20; // OK: D::a = 20

Note that "both" B and C have to be virtual, otherwise if one of them is non-virtual, D would have a virtual A subobject and another non-virtual A subobject, and ambiguity will be still taken place even if class D itself is virtual. For example, class D is ambiguous in all of the following:

struct A { int a; };
struct B : A { int b; };
struct C : virtual A { int c; };
struct D : B, C {};

Or

struct A { int a; };
struct B : virtual A { int b; };
struct C : A { int c; };
struct D : B, C {};

Or

struct A { int a; };
struct B : A { int b; };
struct C : virtual A { int c; };
struct D : virtual B, C {};

Or

struct A { int a; };
struct B : virtual A { int b; };
struct C : A { int c; };
struct D : virtual B, C {};
Unroof answered 12/5, 2022 at 6:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.