Cargando...

Herencia múltiple y el problema del diamante en C++

Aprende la herencia múltiple y el problema del diamante en C++, incluyendo herencia virtual y soluciones prácticas.

C++ admite la herencia múltiple: una clase puede heredar de más de una clase base. Sin embargo, esta flexibilidad también introduce complejidad, especialmente cuando la misma clase base se hereda a través de distintas ramas, lo que genera el problema del diamante (diamond problem). En este artículo veremos qué es este problema, por qué ocurre, sus síntomas y cómo resolverlo mediante la herencia virtual.


1. ¿Qué es el problema del diamante?

Consideremos la siguiente jerarquía:


       Publication
        /      \
    EBook    PrintedBook
        \      /
       HybridBook

HybridBook hereda de EBook y PrintedBook. Ambas clases derivan de Publication, por lo que HybridBook contendrá dos subobjetos Publication separados. El resultado es ambigüedad, duplicación y mayor uso de memoria.


2. Herencia no virtual (problema original)

Veamos el problema en código:


#include <iostream>
#include <string>
using namespace std;

class Publication {
protected:
    string title;
public:
    Publication(string t) : title(t) {}
    void PrintTitle() const { cout << "Título: " << title << endl; }
};

class EBook : public Publication {
public:
    EBook(string t) : Publication(t) {}
};

class PrintedBook : public Publication {
public:
    PrintedBook(string t) : Publication(t) {}
};

class HybridBook : public EBook, public PrintedBook {
public:
    HybridBook(string te, string tp) : EBook(te), PrintedBook(tp) {}
};

int main() {
    HybridBook hb("Título E", "Título P");

    // Ambigüedad: hay dos subobjetos Publication
    // hb.PrintTitle(); // ERROR: 'PrintTitle' es ambiguo

    hb.EBook::PrintTitle();       // Título: Título E
    hb.PrintedBook::PrintTitle(); // Título: Título P
}

Como se puede ver, HybridBook contiene dos instancias diferentes de Publication. Esto causa duplicación y ambigüedad al acceder a métodos o datos compartidos.


3. Solución: Herencia virtual

Si ambas clases intermedias (EBook y PrintedBook) heredan de la clase base de manera virtual, la clase más derivada (HybridBook) contendrá una única copia compartida de la clase base.


class Publication {
protected:
    string title;
public:
    Publication(string t) : title(t) { cout << "Publication()" << endl; }
    void PrintTitle() const { cout << "Título: " << title << endl; }
};

// Nota: herencia virtual pública
class EBook : virtual public Publication {
public:
    EBook(string t) : Publication(t) { cout << "EBook()" << endl; }
};

class PrintedBook : virtual public Publication {
public:
    PrintedBook(string t) : Publication(t) { cout << "PrintedBook()" << endl; }
};

class HybridBook : public EBook, public PrintedBook {
public:
    // La clase más derivada llama al constructor de la base virtual
    HybridBook(string t)
        : Publication(t), EBook(t), PrintedBook(t) {
        cout << "HybridBook()" << endl;
    }
};

Ahora HybridBook tiene una sola instancia de Publication. La ambigüedad desaparece:


int main() {
    HybridBook hb("Título unificado");
    hb.PrintTitle(); // Llamada única a Publication
}

Importante: la clase más derivada (HybridBook) siempre es responsable de inicializar la base virtual.


4. Orden de construcción y base virtual

Con herencia virtual, los constructores se ejecutan en el siguiente orden:

  1. Primero las bases virtuales (solo una copia).
  2. Luego las clases intermedias (EBook, PrintedBook).
  3. Por último, la clase más derivada (HybridBook).

Además, solo la clase más derivada inicializa la base virtual:


class Publication {
public:
    string title;
    Publication(string t) : title(t) { cout << "Publication()" << endl; }
};

class EBook : virtual public Publication {
public:
    EBook() : Publication("dummy-ignored") { cout << "EBook()" << endl; }
};

class PrintedBook : virtual public Publication {
public:
    PrintedBook() : Publication("dummy-ignored") { cout << "PrintedBook()" << endl; }
};

class HybridBook : public EBook, public PrintedBook {
public:
    HybridBook(string t) : Publication(t), EBook(), PrintedBook() {
        cout << "HybridBook()" << endl;
    }
};

int main() {
    HybridBook hb("Unificado");
    cout << hb.title << endl; // "Unificado"
}

5. Ambigüedades y soluciones

Incluso con herencia virtual, los métodos con el mismo nombre pueden causar ambigüedades. Las soluciones incluyen:


struct A { void Info() const { cout << "A" << endl; } };
struct B : virtual A { void Info() const { cout << "B" << endl; } };
struct C : virtual A { void Info() const { cout << "C" << endl; } };

struct D : B, C {
    void Info() const { cout << "D (unificado)" << endl; }
};

6. Ejemplo en el ecosistema de libros

Un ejemplo más realista: BookBase es una clase base abstracta, EBook y PrintedBook heredan virtualmente, y HybridBook combina ambos modelos de precio.


#include <iostream>
#include <string>
using namespace std;

class BookBase {
protected:
    string title; string author;
public:
    BookBase(string t, string a) : title(t), author(a) {}
    virtual ~BookBase() = default;

    virtual double GetBasePrice() const = 0;
    virtual void Print() const {
        cout << title << " - " << author << endl;
    }
};

class EBook : virtual public BookBase {
protected:
    double digitalPrice; double vat;
public:
    EBook(string t, string a, double price, double v)
        : BookBase(t,a), digitalPrice(price), vat(v) {}
    double GetBasePrice() const override { return digitalPrice*(1+vat); }
};

class PrintedBook : virtual public BookBase {
protected:
    double printBase; int pages; bool hardCover;
public:
    PrintedBook(string t, string a, double base, int p, bool hard)
        : BookBase(t,a), printBase(base), pages(p), hardCover(hard) {}
    double GetBasePrice() const override {
        double cost = printBase + pages*0.03;
        if (hardCover) cost *= 1.1;
        return cost;
    }
};

class HybridBook : public EBook, public PrintedBook {
public:
    HybridBook(string t, string a,
               double digital, double vat,
               double printBase, int p, bool hard)
        : BookBase(t,a),
          EBook(t,a,digital,vat),
          PrintedBook(t,a,printBase,p,hard) {}

    double GetFinalPrice() const {
        return 0.6*EBook::GetBasePrice() + 0.4*PrintedBook::GetBasePrice();
    }

    void Print() const override {
        BookBase::Print();
        cout << "Precio final: " << GetFinalPrice() << " €" << endl;
    }
};

int main() {
    HybridBook hb("Modern C++", "A. Dev",
                  100.0, 0.1, 50.0, 400, true);
    hb.Print();
}

7. Notas de diseño y alternativas


8. TL;DR

  • Problema del diamante: la misma clase base heredada dos veces → copias duplicadas, ambigüedad y redundancia.
  • Herencia virtual → mantiene una sola copia compartida y elimina la ambigüedad.
  • La clase más derivada llama al constructor de la base virtual.
  • Resuelva ambigüedades con calificación (A::Foo), override o using.
  • Prefiera la composición o las interfaces abstractas sobre la herencia múltiple siempre que sea posible.
  • Todos los ejemplos son compatibles con Visual Studio 2022 y GCC 11+.

Artículos relacionados