¿Cuál es el problema del diamante en C ++? Cómo detectarlo y cómo solucionarlo

La herencia múltiple en C ++ es poderosa, pero una herramienta complicada, que a menudo genera problemas si no se usa con cuidado, problemas como el problema del diamante.

En este artículo, analizaremos el problema del diamante, cómo surge de la herencia múltiple y qué puede hacer para resolver el problema.

Herencia múltiple en C ++

La herencia múltiple es una característica de la programación orientada a objetos (OOP) donde una subclase puede heredar de más de una superclase. En otras palabras, una clase secundaria puede tener más de un padre.

La siguiente figura muestra una representación pictórica de múltiples herencias.

En el diagrama anterior, la clase C tiene la clase A y la clase B como padres.

Si consideramos un escenario de la vida real, un niño hereda de su padre y su madre. Por tanto, un Niño puede representarse como una clase derivada con "Padre" y "Madre" como sus padres. De manera similar, podemos tener muchos ejemplos de la vida real de herencia múltiple.

En la herencia múltiple, los constructores de una clase heredada se ejecutan en el orden en que se heredan. Por otro lado, los destructores se ejecutan en el orden inverso de su herencia.

Ahora ilustremos la herencia múltiple y verifiquemos el orden de construcción y destrucción de los objetos.

Ilustración de código de herencia múltiple

Para la ilustración de herencia múltiple, hemos programado exactamente la representación anterior en C ++. El código del programa se proporciona a continuación.

 #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;
}

La salida que obtenemos del programa anterior es la siguiente:

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

Ahora, si comprobamos la salida, vemos que los constructores se llaman en el orden B, A y C mientras que los destructores están en el orden inverso. Ahora que conocemos los conceptos básicos de la herencia múltiple, pasamos a discutir el problema del diamante.

El problema del diamante, explicado

El problema del diamante ocurre cuando una clase secundaria hereda de dos clases principales que comparten una clase de abuelos común. Esto se ilustra en el diagrama siguiente:

Aquí, tenemos una clase Niño que hereda de las clases Padre y Madre . Estas dos clases, a su vez, heredan la clase Persona porque tanto el Padre como la Madre son Persona.

Como se muestra en la figura, la clase Niño hereda los rasgos de la clase Persona dos veces: una del Padre y otra de la Madre. Esto da lugar a ambigüedad ya que el compilador no comprende qué camino tomar.

Este escenario da lugar a un gráfico de herencia en forma de diamante y se conoce como "El problema del diamante".

Ilustración de código del problema del diamante

A continuación, hemos representado el ejemplo anterior de herencia en forma de diamante mediante programación. El código se proporciona a continuación:

 #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);
}

A continuación se muestra el resultado de este programa:

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

Ahora puedes ver la ambigüedad aquí. El constructor de la clase Person se llama dos veces: una vez cuando se crea el objeto de la clase Padre y luego cuando se crea el objeto de la clase Madre. Las propiedades de la clase Person se heredan dos veces, lo que genera ambigüedad.

Dado que el constructor de la clase Person se llama dos veces, el destructor también se llamará dos veces cuando se destruya el objeto de la clase Child.

Ahora, si ha entendido el problema correctamente, analicemos la solución al problema del diamante.

Cómo solucionar el problema del diamante en C ++

La solución al problema del diamante es utilizar la palabra clave virtual . Convertimos las dos clases principales (que heredan de la misma clase de abuelos) en clases virtuales para evitar dos copias de la clase de abuelos en la clase secundaria.

Cambiemos la ilustración anterior y verifiquemos el resultado:

Ilustración de código para solucionar el problema del diamante

 #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);
}

Aquí hemos utilizado la palabra clave virtual cuando las clases Padre y Madre heredan la clase Persona. Esto generalmente se denomina "herencia virtual", lo que garantiza que solo se transmita una única instancia de la clase heredada (en este caso, la clase Person).

En otras palabras, la clase Child tendrá una única instancia de la clase Person, compartida por las clases Father y Mother. Al tener una sola instancia de la clase Person, se resuelve la ambigüedad.

La salida del código anterior se da a continuación:

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

Aquí puede ver que el constructor de la clase Person se llama solo una vez.

Una cosa a tener en cuenta sobre la herencia virtual es que incluso si el constructor parametrizado de la clase Person es llamado explícitamente por los constructores de la clase Father y Mother a través de listas de inicialización, solo se llamará al constructor base de la clase Person .

Esto se debe a que solo hay una instancia única de una clase base virtual compartida por varias clases que heredan de ella.

Para evitar que el constructor base se ejecute varias veces, la clase que la hereda no llama al constructor de una clase base virtual. En cambio, el constructor es llamado por el constructor de la clase concreta.

En el ejemplo anterior, la clase Child llama directamente al constructor base para la clase Person.

Relacionado: Guía para principiantes de la biblioteca de plantillas estándar en C ++

¿Qué sucede si necesita ejecutar el constructor parametrizado de la clase base? Puede hacerlo llamándolo explícitamente en la clase Niño en lugar de en las clases Padre o Madre.

El problema del diamante en C ++, resuelto

El problema del diamante es una ambigüedad que surge en la herencia múltiple cuando dos clases principales heredan de la misma clase abuela y ambas clases principales son heredadas por una sola clase secundaria. Sin el uso de la herencia virtual, la clase secundaria heredaría las propiedades de la clase abuelo dos veces, lo que generaría ambigüedad.

Esto puede surgir con frecuencia en el código del mundo real, por lo que es importante abordar esa ambigüedad cada vez que se detecta.

El problema del diamante se soluciona mediante la herencia virtual, en la que se utiliza la palabra clave virtual cuando las clases principales heredan de una clase abuela compartida. Al hacerlo, solo se realiza una copia de la clase de abuelos, y la clase secundaria realiza la construcción del objeto de la clase de abuelos.