Plusieurs classes de base

Une classe peut être dérivée de plusieurs classes de base. Dans un modèle à héritage multiple (où les classes sont dérivées de plusieurs classes de base), les classes de base sont spécifiées à l’aide de l’élément de grammaire de liste de base. Par exemple, la déclaration de classe pour CollectionOfBook, dérivé de Collection et Book, peut être spécifiée :

// deriv_MultipleBaseClasses.cpp
// compile with: /LD
class Collection {};
class Book {};
class CollectionOfBook : public Book, public Collection {
    // New members
};

L’ordre dans lequel les classes de base sont spécifiées n’est pas significatif, sauf dans certains cas où les constructeurs et les destructeurs sont appelés. Dans ces cas, l'ordre dans lequel les classes de base sont spécifiées a une incidence sur ce qui suit :

  • Ordre dans lequel les constructeurs sont appelés. Si votre code repose sur la partie Book de CollectionOfBook à initialiser avant la partie Collection, l'ordre des spécifications est important. L’initialisation a lieu dans l’ordre dans lequel les classes sont spécifiées dans la liste de base.

  • L'ordre dans lequel les destructeurs sont appelés pour effectuer le nettoyage. Là encore, si un « élément » particulier de la classe doit être présent lorsque l'autre élément est détruit, l'ordre a une importance. Les destructeurs sont appelés dans l’ordre inverse des classes spécifiées dans la liste de base.

    Remarque

    L'ordre de spécification des classes de base peut avoir une incidence sur la disposition de mémoire de la classe. Ne prenez aucune décision de programmation concernant l'ordre des membres de base en mémoire.

Lorsque vous spécifiez la liste de base, vous ne pouvez pas spécifier le même nom de classe plusieurs fois. Toutefois, il est possible qu’une classe soit une base indirecte à une classe dérivée plusieurs fois.

Classes de base virtuelles

Comme une classe peut être une classe de base indirecte à une classe dérivée plusieurs fois, C++ offre un moyen d'optimiser la façon dont fonctionnent de telles classes de base. Les classes de base virtuelles permettent d'économiser de l'espace et d'éviter les ambiguïtés dans les hiérarchies de classes qui utilisent un héritage multiple.

Chaque objet non virtuel contient une copie des données membres définies dans la classe de base. Cette duplication gaspille de l'espace et vous oblige à spécifier quelle copie des membres de classe de base vous souhaitez chaque fois que vous accédez à eux.

Lorsqu'une classe de base est spécifiée comme base virtuelle, elle peut agir comme base indirecte plusieurs fois sans duplication de ses données membres. Une copie unique de ses données membres est partagée par toutes les classes de base qui l'utilisent comme base virtuelle.

Lors de la déclaration d’une classe de base virtuelle, le virtual mot clé apparaît dans les listes de base des classes dérivées.

Considérez la hiérarchie de classes dans la figure suivante, qui illustre une ligne de déjeuner simulée :

Diagramme d’une ligne de déjeuner simulée.

La classe de base est Queue. File d’attente de caisse et file d’attente de déjeuner héritent tous deux de la file d’attente. Enfin, la file d’attente lunch cashier hérite à la fois de la file d’attente du caissier et de la file d’attente du déjeuner.

Graphique en courbes de déjeuner simulé

Dans la figure, Queue représente la classe de base de CashierQueue et LunchQueue. Toutefois, lorsque les deux classes sont combinées pour former LunchCashierQueue, le problème suivant survient : la nouvelle classe contient deux sous-objets de type Queue, l'un provenant de CashierQueue et l'autre de LunchQueue. La figure suivante montre la disposition conceptuelle de la mémoire (la disposition de mémoire réelle peut être optimisée) :

Diagramme d’un objet de ligne de déjeuner simulé.

La figure montre un objet Lunch Cashier Queue avec deux sous-objets dans celui-ci : File d’attente de cashier et File d’attente de déjeuner. La file d’attente cashier et la file d’attente du déjeuner contiennent un sous-objet File d’attente.

Objet de ligne de déjeuner simulé

Il existe deux Queue sous-objets dans l’objet LunchCashierQueue . Le code suivant déclare Queue en tant que classe de base virtuelle :

// deriv_VirtualBaseClasses.cpp
// compile with: /LD
class Queue {};
class CashierQueue : virtual public Queue {};
class LunchQueue : virtual public Queue {};
class LunchCashierQueue : public LunchQueue, public CashierQueue {};

Le virtual mot clé garantit qu’une seule copie du sous-objet Queue est incluse (voir la figure suivante).

Diagramme d’un objet de ligne de déjeuner simulé, avec des classes de base virtuelles représentées.

Le diagramme montre un objet Lunch Cashier Queue, qui contient un sous-objet File d’attente cashier et un sous-objet File d’attente déjeuner. La file d’attente cashier et la file d’attente déjeuner partagent le même sous-objet File d’attente.

Objet de ligne de déjeuner simulé avec des classes de base virtuelles

Une classe peut avoir à la fois un composant virtuel et un composant non virtuel d'un type donné. Cela se produit dans les conditions illustrées dans la figure suivante :

Diagramme des composants virtuels et non virtuels d’une classe.

Le diagramme montre une classe de base de file d’attente. Une classe File d’attente cashier et une classe File d’attente déjeuner héritent virtuellement de File d’attente. Une troisième classe, Takeout Queue, hérite non pratiquement de la file d’attente. Lunch Cashier Queue hérite à la fois de la file d’attente cashier et de la file d’attente déjeuner. Lunch Takeout Cashier Queue hérite à la fois de lunch Cashier Queue et Takeout Queue.

Composants virtuels et non virtuels de la même classe

Dans cette figure, CashierQueue et LunchQueue utilisent Queue comme classe de base virtuelle. Toutefois, TakeoutQueue spécifie Queue en tant que classe de base, et non pas comme classe de base virtuelle. Par conséquent, LunchTakeoutCashierQueue a deux sous-objets de type Queue : l’un provenant du chemin d’héritage qui inclut LunchCashierQueue et l’autre provenant du chemin qui inclut TakeoutQueue. La figure ci-dessous illustre cela.

Diagramme de la disposition d’objet pour l’héritage virtuel et non virtuel.

Un objet Lunch Takeout Cashier Queue est montré qui contient deux sous-objets : une file d’attente de takeout (qui contient un sous-objet File d’attente) et une file d’attente de cashier déjeuner. Le sous-objet File d’attente Lunch Cashier contient un sous-objet File d’attente cashier et un sous-objet File d’attente déjeuner, qui partagent tous deux un sous-objet File d’attente.

Disposition d’objets avec héritage virtuel et non virtuel

Remarque

L'héritage virtuel fournit des avantages de taille significatifs par rapport à l'héritage non virtuel. Toutefois, il peut introduire une certaine surcharge de traitement.

Si une classe dérivée remplace une fonction virtuelle qu’elle hérite d’une classe de base virtuelle et si un constructeur ou un destructeur pour la classe de base dérivée appelle cette fonction à l’aide d’un pointeur vers la classe de base virtuelle, le compilateur peut introduire d’autres champs « vtordisp » masqués dans les classes avec des bases virtuelles. L’option /vd0 du compilateur supprime l’ajout du constructeur vtordisp masqué/membre de déplacement du destructeur. L’option /vd1 du compilateur, par défaut, les active là où elles sont nécessaires. Désactivez vtordisps uniquement si vous êtes sûr que tous les constructeurs de classe et destructeurs appellent virtuellement des fonctions virtuelles.

L’option /vd du compilateur affecte l’ensemble d’un module de compilation. Utilisez le vtordisp pragma pour supprimer et réactiver vtordisp les champs sur une base classe par classe :

#pragma vtordisp( off )
class GetReal : virtual public { ... };
#pragma vtordisp( on )

Ambiguïtés au niveau du nom

L'héritage multiple présente le risque que les noms soient hérités le long de plusieurs chemins. Les noms de membres de classe le long de ces chemins ne sont pas nécessairement uniques. Ces conflits de nom sont appelés « ambiguïtés. »

Toute expression qui fait référence à un membre de classe doit faire une référence qui ne soit pas ambigu. L'exemple suivant montre comment développer les ambiguïtés :

// deriv_NameAmbiguities.cpp
// compile with: /LD
// Declare two base classes, A and B.
class A {
public:
    unsigned a;
    unsigned b();
};

class B {
public:
    unsigned a();  // class A also has a member "a"
    int b();       //  and a member "b".
    char c;
};

// Define class C as derived from A and B.
class C : public A, public B {};

Étant donné les déclarations de classe précédentes, le code tel que le suivant est ambigu, car il n’est pas clair s’il b fait référence à l’entrée b ou à l’entrée AB:

C *pc = new C;

pc->b();

Prenons l'exemple précédent. Étant donné que le nom a est membre de la classe et de la classe AB, le compilateur ne peut pas discerner qui a désigne la fonction à appeler. L'accès à un membre est ambigu s'il peut faire référence à plusieurs fonctions, objets, types ou énumérateurs.

Le compilateur détecte les ambiguïtés en exécutant des tests dans cet ordre :

  1. Si l'accès au nom est ambigu (comme il vient d'être décrit), un message d'erreur est généré.

  2. Si les fonctions surchargées ne sont pas ambiguës, elles sont résolues.

  3. Si l'accès au nom ne respecte pas l'autorisation accès-membre, un message d'erreur est généré. Pour plus d’informations, consultez Member-Access Contrôle.

Lorsqu'une expression génère une ambiguïté par héritage, vous pouvez manuellement la résoudre en qualifiant le nom en question avec son nom de classe. Pour que l'exemple précédent se compile correctement sans ambiguïtés, utilisez par exemple ce code :

C *pc = new C;

pc->B::a();

Remarque

Lorsque C est déclaré, il a la possibilité de provoquer des erreurs lorsque B est référencé dans la portée de C. Aucune erreur n'est publiée, toutefois, jusqu'à ce qu'une référence non qualifiée à B soit effectuée en réalité dans la portée de C.

Dominance

Il est possible que plusieurs noms (fonction, objet ou énumérateur) soient atteints via un graphique d’héritage. Ces cas sont jugés équivoques par rapport aux classes de base non virtuelles. Ils sont également ambigus avec les classes de base virtuelles, sauf si l’un des noms « domine » les autres.

Un nom domine un autre nom s’il est défini dans les deux classes et qu’une classe est dérivée de l’autre. Le nom dominant est le nom figurant dans la classe dérivée. Ce nom est utilisé lorsqu'une ambiguïté serait autrement apparue, comme indiqué dans l'exemple suivant :

// deriv_Dominance.cpp
// compile with: /LD
class A {
public:
    int a;
};

class B : public virtual A {
public:
    int a();
};

class C : public virtual A {};

class D : public B, public C {
public:
    D() { a(); } // Not ambiguous. B::a() dominates A::a.
};

Conversions ambiguës

Les conversions explicites et implicites à partir de pointeurs ou de références vers des types classe peuvent provoquer des ambiguïtés. L'illustration suivante, Conversion ambiguë des pointeurs vers des classes de base, montre ce qui suit :

  • La déclaration d'un objet de type D.

  • Effet de l’application de l’opérateur d’adresse (&) à cet objet. L’opérateur address-of fournit toujours l’adresse de base de l’objet.

  • L'effet de la conversion explicite du pointeur obtenu à l'aide de l'opérateur d'adresse vers le type de classe de base A. Le fait de coéquer l’adresse de l’objet à taper A* ne fournit pas toujours au compilateur suffisamment d’informations quant au sous-objet de type A à sélectionner ; dans ce cas, deux sous-objets existent.

Diagramme montrant comment la conversion de pointeurs en classes de base peut être ambiguë.

Le diagramme montre d’abord une hiérarchie d’héritage : A est la classe de base. B et C héritent de A. D hérite de B et C. Ensuite, la disposition de la mémoire est affichée pour l’objet D. Il existe trois sous-objets dans D : B (qui inclut un sous-objet A) et C (qui inclut un sous-objet A). Le code &d pointe vers l’objet A dans le sous-objet B. Le code ( * A ) & d pointe vers le sous-objet B et le sous-objet C.

Conversion ambiguë de pointeurs en classes de base

La conversion en type A* (pointeur vers A) est ambiguë, car il n’existe aucun moyen de discerner quel sous-objet de type A est le bon. Vous pouvez éviter l’ambiguïté en spécifiant explicitement le sous-objet que vous voulez utiliser, comme suit :

(A *)(B *)&d       // Use B subobject.
(A *)(C *)&d       // Use C subobject.

Ambiguïtés et classes de base virtuelles

Si des classes de base virtuelles sont utilisées, les fonctions, les objets, les types et les énumérateurs sont accessibles via plusieurs chemins d’héritage. Étant donné qu’il n’existe qu’une seule instance de la classe de base, il n’y a aucune ambiguïté lors de l’accès à ces noms.

L'illustration suivante montre comment les objets sont composés à l'aide de l'héritage virtuel et non virtuel.

Diagramme montrant la dérivation virtuelle et la dérivation non virtuelle.

Le diagramme montre d’abord une hiérarchie d’héritage : A est la classe de base. B et C héritent pratiquement de A. D hérite pratiquement de B et C. Ensuite, la disposition de D s’affiche. D contient des sous-objets B et C, qui partagent le sous-objet A. Ensuite, la disposition s’affiche comme si la même hiérarchie avait été dérivée à l’aide de l’héritage non virtuel. Dans ce cas, D contient les sous-objets B et C. B et C contiennent leur propre copie de sous-objet A.

Dérivation virtuelle et non virtuelle

Dans l'illustration, accéder à un membre de classe A via des classes de base non virtuelles provoque une ambiguïté ; le compilateur ne propose aucune information indiquant s'il convient d'utiliser le sous-objet associé à B ou le sous-objet associé à C. Toutefois, lorsqu’elle A est spécifiée en tant que classe de base virtuelle, il n’existe aucune question sur laquelle le sous-objet est accessible.

Voir aussi

Héritage