Как я могу избежать Алмаза Смерти при использовании множественного наследования?
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 {};