Héritage multiple et problème du diamant en C++
Apprenez l’héritage multiple et le problème du diamant en C++, avec l’héritage virtuel et des solutions pratiques.
C++ prend en charge l’héritage multiple : une classe peut hériter de plusieurs classes de base. Cette flexibilité apporte de la puissance, mais aussi de la complexité — notamment lorsque la même classe de base est héritée plusieurs fois par des chemins différents, ce qui crée le problème du diamant (diamond problem). Dans cet article, nous verrons ce qu’est ce problème, pourquoi il survient, ses effets, et comment le résoudre grâce à l’héritage virtuel (virtual inheritance).
1. Qu’est-ce que le problème du diamant ?
Considérons la hiérarchie suivante :
Publication
/ \
EBook PrintedBook
\ /
HybridBook
HybridBook hérite à la fois de EBook et de PrintedBook.
Comme ces deux classes dérivent de Publication,
HybridBook contiendra deux sous-objets Publication distincts.
Résultat : ambiguïté, duplication et consommation mémoire supplémentaire.
2. Héritage non virtuel (problématique)
Voyons le problème en pratique :
#include <iostream>
#include <string>
using namespace std;
class Publication {
protected:
string title;
public:
Publication(string t) : title(t) {}
void PrintTitle() const { cout << "Titre : " << 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("E-Titre", "P-Titre");
// Ambiguïté : deux objets Publication distincts
// hb.PrintTitle(); // ERREUR : appel ambigu
hb.EBook::PrintTitle(); // Titre : E-Titre
hb.PrintedBook::PrintTitle(); // Titre : P-Titre
}
Comme on le voit, HybridBook contient deux sous-objets Publication.
Cela entraîne une duplication et des ambiguïtés dans les appels de méthodes et les membres communs.
3. La solution : l’héritage virtuel
Si les classes intermédiaires (EBook et PrintedBook) héritent de la même classe de base
de manière virtuelle, la classe la plus dérivée (HybridBook)
ne contiendra qu’une seule instance partagée de la classe de base.
class Publication {
protected:
string title;
public:
Publication(string t) : title(t) { cout << "Publication()" << endl; }
void PrintTitle() const { cout << "Titre : " << title << endl; }
};
// Remarque : héritage virtuel public
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 classe la plus dérivée appelle le constructeur de la base virtuelle
HybridBook(string t)
: Publication(t), EBook(t), PrintedBook(t) {
cout << "HybridBook()" << endl;
}
};
Désormais, HybridBook ne contient qu’un seul sous-objet Publication.
L’ambiguïté est donc levée :
int main() {
HybridBook hb("Titre unique");
hb.PrintTitle(); // Appel unique via la seule instance Publication
}
Important : la classe la plus dérivée (HybridBook)
est toujours responsable de l’initialisation de la base virtuelle.
4. Ordre de construction et base virtuelle
Lorsqu’on utilise l’héritage virtuel, l’ordre d’appel des constructeurs est le suivant :
- Les bases virtuelles sont construites en premier (une seule copie).
- Ensuite les classes intermédiaires (
EBook,PrintedBook). - Enfin, la classe la plus dérivée (
HybridBook).
De plus, seule la classe la plus dérivée initialise la base virtuelle :
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("Unique");
cout << hb.title << endl; // "Unique"
}
5. Ambiguïtés et moyens de résolution
Même avec l’héritage virtuel, des méthodes portant le même nom peuvent créer une ambiguïté. Solutions possibles :
- Qualification de portée : par ex.
obj.EBook::Foo(). - Override : définir un comportement unifié dans la classe dérivée.
- using : importer une méthode spécifique de la base dans la portée courante.
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 (unifié)" << endl; }
};
6. Exemple : écosystème de livres hybrides
Exemple plus concret :
BookBase est une classe de base abstraite,
EBook et PrintedBook en héritent virtuellement,
et HybridBook combine les deux modèles de prix.
#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 << "Prix final : " << GetFinalPrice() << " €" << endl;
}
};
int main() {
HybridBook hb("Modern C++", "A. Dev",
100.0, 0.1, 50.0, 400, true);
hb.Print();
}
7. Notes de conception et alternatives
- L’héritage multiple est puissant mais aussi complexe. Privilégiez la composition (has-a) si possible.
- Pour partager des comportements, les interfaces virtuelles pures (classes abstraites) sont souvent plus simples.
- En cas d’héritage virtuel, c’est toujours la classe la plus dérivée qui initialise la base virtuelle.
- Pour réduire les ambiguïtés, utilisez override, using ou une qualification explicite du nom.
8. TL;DR
- Problème du diamant : la même classe de base héritée deux fois → deux copies, ambiguïté et redondance.
- Héritage virtuel → une seule instance partagée, ambiguïté supprimée.
- La classe la plus dérivée appelle le constructeur de la base virtuelle.
- Résolvez les ambiguïtés avec une qualification (
A::Foo), override ou using. - Préférez la composition ou les interfaces abstraites à l’héritage multiple lorsque c’est possible.
- Tous les exemples sont compatibles avec Visual Studio 2022 et GCC 11+.