В чем проблема алмазов в C ++? Как это обнаружить и как исправить

Множественное наследование в C ++ – мощный, но хитрый инструмент, который при неосторожном использовании часто приводит к проблемам, например к проблемам типа Diamond Problem.

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

Множественное наследование в C ++

Множественное наследование – это функция объектно-ориентированного программирования (ООП), при которой подкласс может наследовать более чем от одного суперкласса. Другими словами, дочерний класс может иметь более одного родителя.

На рисунке ниже показано графическое изображение множественного наследования.

На приведенной выше диаграмме класс C имеет класс A и класс B в качестве своих родителей.

Если рассматривать реальный сценарий, ребенок наследуется от отца и матери. Таким образом, Child может быть представлен как производный класс с «Отец» и «Мать» в качестве его родителей. Точно так же у нас может быть много таких реальных примеров множественного наследования.

При множественном наследовании конструкторы унаследованного класса выполняются в том порядке, в котором они унаследованы. С другой стороны, деструкторы выполняются в порядке, обратном их наследованию.

Теперь проиллюстрируем множественное наследование и проверим порядок построения и уничтожения объектов.

Кодовая иллюстрация множественного наследования

Для иллюстрации множественного наследования мы точно запрограммировали приведенное выше представление на C ++. Код программы приведен ниже.

 #include<iostream>
using namespace std;
class A //base class A with constructor and destructor
{
public:
A() { cout << "class A::Constructor" << endl; }
~A() { cout << "class A::Destructor" << endl; }
};
class B //base class B with constructor and destructor
{
public:
B() { cout << "class B::Constructor" << endl; }
~B() { cout << "class B::Destructor" << endl; }
};
class C: public B, public A //derived class C inherits class A and then class B (note the order)
{
public:
C() { cout << "class C::Constructor" << endl; }
~C() { cout << "class C::Destructor" << endl; }
};
int main(){
C c;
return 0;
}

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

 class B::Constructor
class A::Constructor
class C::Constructor
class C::Destructor
class A::Destructor
class B::Destructor

Теперь, если мы проверим вывод, мы увидим, что конструкторы вызываются в порядке B, A и C, а деструкторы – в обратном порядке. Теперь, когда мы знаем основы множественного наследования, мы переходим к обсуждению проблемы алмаза.

Разъяснение проблемы алмаза

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

Здесь у нас есть класс Child, унаследованный от классов Father и Mother . Эти два класса, в свою очередь, наследуют класс Person, потому что и Отец, и Мать являются Person.

Как показано на рисунке, класс Child наследует черты класса Person дважды – один раз от отца, а второй – от матери. Это вызывает двусмысленность, поскольку компилятор не понимает, в каком направлении двигаться.

Этот сценарий приводит к появлению ромбовидного графа наследования, получившего известное название «Алмазная проблема».

Кодовая иллюстрация проблемы алмаза

Ниже мы представили приведенный выше пример наследования в форме ромба программно. Код приведен ниже:

 #include<iostream>
using namespace std;
class Person { //class Person
public:
Person(int x) { cout << "Person::Person(int) called" << endl; }
};

class Father : public Person { //class Father inherits Person
public:
Father(int x):Person(x) {
cout << "Father::Father(int) called" << endl;
}
};

class Mother : public Person { //class Mother inherits Person
public:
Mother(int x):Person(x) {
cout << "Mother::Mother(int) called" << endl;
}
};

class Child : public Father, public Mother { //Child inherits Father and Mother
public:
Child(int x):Mother(x), Father(x) {
cout << "Child::Child(int) called" << endl;
}
};

int main() {
Child child(30);
}

Ниже приводится результат работы этой программы:

 Person::Person(int) called
Father::Father(int) called
Person::Person(int) called
Mother::Mother(int) called
Child::Child(int) called

Теперь вы можете увидеть здесь неоднозначность. Конструктор класса Person вызывается дважды: один раз при создании объекта класса «Отец», а затем – при создании объекта класса «Мать». Свойства класса Person наследуются дважды, что вызывает неоднозначность.

Поскольку конструктор класса Person вызывается дважды, деструктор также будет вызываться дважды при разрушении объекта класса Child.

Теперь, если вы правильно поняли проблему, давайте обсудим решение проблемы с бриллиантом.

Как исправить проблему с бриллиантом в C ++

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

Давайте изменим иллюстрацию выше и проверим вывод:

Иллюстрация кода для решения проблемы с алмазом

 #include<iostream>
using namespace std;
class Person { //class Person
public:
Person() { cout << "Person::Person() called" << endl; } //Base constructor
Person(int x) { cout << "Person::Person(int) called" << endl; }
};

class Father : virtual public Person { //class Father inherits Person
public:
Father(int x):Person(x) {
cout << "Father::Father(int) called" << endl;
}
};

class Mother : virtual public Person { //class Mother inherits Person
public:
Mother(int x):Person(x) {
cout << "Mother::Mother(int) called" << endl;
}
};

class Child : public Father, public Mother { //class Child inherits Father and Mother
public:
Child(int x):Mother(x), Father(x) {
cout << "Child::Child(int) called" << endl;
}
};

int main() {
Child child(30);
}

Здесь мы использовали ключевое слово virtual, когда классы Отец и Мать наследуют класс Person. Обычно это называется «виртуальным наследованием», которое гарантирует передачу только одного экземпляра унаследованного класса (в данном случае класса Person).

Другими словами, класс Child будет иметь единственный экземпляр класса Person, совместно используемый классами «Отец» и «Мать». Наличие единственного экземпляра класса Person устраняет неоднозначность.

Вывод приведенного выше кода приведен ниже:

 Person::Person() called
Father::Father(int) called
Mother::Mother(int) called
Child::Child(int) called

Здесь вы можете видеть, что конструктор класса Person вызывается только один раз.

В отношении виртуального наследования следует отметить, что даже если параметризованный конструктор класса Person явно вызывается конструкторами классов Отец и Мать через списки инициализации, будет вызываться только базовый конструктор класса Person .

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

Чтобы предотвратить многократное выполнение базового конструктора, конструктор виртуального базового класса не вызывается классом, наследующим от него. Вместо этого конструктор вызывается конструктором конкретного класса.

В приведенном выше примере класс Child напрямую вызывает базовый конструктор для класса Person.

Связано: Руководство для начинающих по стандартной библиотеке шаблонов в C ++

Что, если вам нужно выполнить параметризованный конструктор базового класса? Вы можете сделать это, явно вызвав его в классе Child, а не в классах «Отец» или «Мать».

Проблема алмаза в C ++, решена

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

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

Проблема ромба решается с помощью виртуального наследования, в котором ключевое слово virtual используется, когда родительские классы наследуются от общего класса прародителя. Таким образом создается только одна копия класса grandparent, а создание объекта класса grandparent выполняется дочерним классом.