Как я могу избежать Алмаза Смерти при использовании множественного наследования?

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

Я знаю, что это значит, но какие шаги я могу предпринять, чтобы избежать этого?

9 ответов

Решение

Практический пример:

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

Обратите внимание, как класс D наследуется от обоих B & C. Но оба B & C наследуются от A. Это приведет к тому, что 2 копии класса A будут включены в vtable.

Чтобы решить это, нам нужно виртуальное наследование. Это класс А, который должен быть фактически унаследован. Итак, это решит проблему:

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

Я бы придерживался только множественного наследования интерфейсов. Хотя множественное наследование классов иногда является привлекательным, оно также может быть запутанным и болезненным, если вы регулярно на него полагаетесь.

Виртуальное наследование. Вот для чего это.

Наследование - это сильное, сильное оружие. Используйте его только тогда, когда вам это действительно нужно. В прошлом наследование бриллиантов было признаком того, что я далеко иду от классификации, говоря, что пользователь - это "сотрудник", но они также являются "слушателем виджетов", но также...

В этих случаях легко решить несколько проблем наследования.

Я решил их, используя композицию и указатели обратно владельцу:

До:

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

После:

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

     WidgetListener listener;
     LectureAttendee attendee;
};

Да, права доступа различны, но если вам удастся избежать такого подхода без дублирования кода, лучше, потому что он менее мощный. (Вы можете сэкономить энергию, когда у вас нет альтернативы.)

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

При этом атрибуты класса A повторяются дважды в классе D, что увеличивает использование памяти... Поэтому для экономии памяти мы создаем виртуальный атрибут для всех унаследованных атрибутов класса A, которые хранятся в Vtable.

Что ж, отличительная черта Dreaded Diamond в том, что это ошибка, когда она возникает. Лучший способ избежать этого - заранее выяснить структуру вашего наследования. Например, у одного проекта, над которым я работаю, есть Зрители и Редакторы. Редакторы являются логическими подклассами Viewers, но поскольку все Viewers являются подклассами - TextViewer, ImageViewer и т. Д., Editor не является производным от Viewer, что позволяет конечным классам TextEditor, ImageEditor избегать алмаза.

В тех случаях, когда алмаз невозможно избежать, используется виртуальное наследование. Однако самое большое предостережение, касающееся виртуальных баз, заключается в том, что конструктор для виртуальной базы должен вызываться наиболее производным классом, а это означает, что производный класс практически не контролирует параметры конструктора. Кроме того, наличие виртуальной базы имеет тенденцию к снижению производительности / пространства при наведении на цепочку, хотя я не верю, что существует большая часть штрафа за большее, чем первое.

Кроме того, вы всегда можете использовать алмаз, если вы точно знаете, какую базу вы хотите использовать. Иногда это единственный способ.

Я бы предложил лучший дизайн класса. Я уверен, что есть некоторые проблемы, которые лучше всего решаются с помощью множественного наследования, но сначала проверьте, есть ли другой способ.

Если нет, используйте виртуальные функции / интерфейсы.

Используйте наследование делегированием. Тогда оба класса будут указывать на базу A, но должны реализовывать методы, перенаправляющие на A. Это побочный эффект превращения защищенных членов A в "частные" члены в B,C и D, но теперь вы этого не делаете. нужен виртуальный, а у тебя нет бриллианта.

Это все, что у меня есть в моих заметках на эту тему. Я думаю, это поможет вам.

Алмазная проблема — это неоднозначность, возникающая, когда два класса B и C наследуют от A, а класс D наследует от B и C. Если в A есть элемент, который B и C, и D не переопределяет его, то наследует ли D: B или 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'

В приведенном выше примере и B, и C наследуют A, и они оба имеют одну копию A. Однако D наследует и B, и C, поэтому D имеет две копии A, одну от B, а другую от C. Если нам нужно доступ к элементу данных an из A через объект D, мы должны указать путь, по которому будет осуществляться доступ к члену: из B или C, потому что большинство компиляторов не могут различить две копии A в D.

Есть 4 способа избежать этой двусмысленности:

1- Используя оператор разрешения области, мы можем вручную указать путь, по которому будет осуществляться доступ к члену данных, но обратите внимание, что все еще есть две копии (два отдельных субъекта) A в D, поэтому проблема все еще существует.

      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- Используя static_cast, мы можем указать, какой путь компилятор может использовать для доступа к члену данных, но обратите внимание, что все еще есть две копии (два отдельных подобъекта) A в D, поэтому проблема все еще существует.

      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- Используя переопределение, неоднозначный класс может переопределить член, но обратите внимание, что все еще есть две копии (два отдельных подобъекта) A в D, поэтому проблема все еще существует.

      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- Используя виртуальное наследование, проблема полностью решена: если наследование от A к B и наследование от A к C помечены как «виртуальные», C++ уделяет особое внимание созданию только одного подобъекта A,

      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

Обратите внимание, что « оба » B и C должны быть виртуальными, иначе, если один из них не виртуальный, D будет иметь виртуальный подобъект A и другой не виртуальный подобъект A, и неоднозначность все равно будет иметь место, даже если сам класс D является виртуальным. Например, класс D неоднозначен во всем следующем:

      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 {};
Другие вопросы по тегам