A priori, BiDiCounter
a besoin à la fois de la méthode increment
qui est défini dans ForwardCounter
et de la méthode decrement
qui est défini dans BackwardCounter
. Il serait tentant de dire, puisque C++ supporte l'héritage multiple, que BiDiCounter
hérite à la fois de ForwardCounter
et de BackwardCounter
.
Cependant, nous allons avoir quelques soucis, puisque ForwardCounter
hérite de BaseCounter
et donc possède :
une propre instance des champs counter
et max
qui sont définis dans la classe de base BaseCounter
que je vais noter ForwardCounter
→BaseCounter
→counter
et ForwardCounter
→BaseCounter
→max
des fonctions getCounter
, getMax
, reset
set
, setMax
qui vont modifier les champs précédents ForwardCounter
→BaseCounter
→counter
et ForwardCounter
→BaseCounter
→max
, nous allons identifier ces fonctions par leur chemin d'accès ForwardCounter
→BaseCounter
→getCounter
, …
une fonctions increment
qui va modifier le champ ForwardCounter
→BaseCounter
→counter
.
De même la classe BackwardCounter
hérite de BaseCounter
est possède :
une propre instance des champs counter
et max
qui sont définis dans la classe de base BaseCounter
que je vais noter BackwardCounter
→BaseCounter
→counter
et BackwardCounter
→BaseCounter
→max
des fonctions getCounter
, getMax
, reset
set
, setMax
qui vont modifier les champs précédents BackwardCounter
→BaseCounter
→counter
et BackwardCounter
→BaseCounter
→max
, nous allons identifier ces fonctions par leur chemin d'accès BackwardCounter
→BaseCounter
→getCounter
, …
une fonctions decrement
qui va modifier le champ BackwardCounter
→BaseCounter
→counter
.
Ceci signifie que nous avons deux instances des champs max
et counter
en fonction de l'héritage :
soit ForwardCounter
→BaseCounter
→max
ou BackwardCounter
→BaseCounter
→max
,
soit ForwardCounter
→BaseCounter
→counter
ou BackwardCounter
→BaseCounter
→counter
.
Plus embêtant, la fonction increment
modifie le champ ForwardCounter
→BaseCounter
→>counter
, la fonction decrement
modifier le champ BackwardCounter
→BaseCounter
→>counter
, ce qui fait que la fonction increment
ne pourra jamais travailler de concert avec la fonction decrement
puisqu'elles ne partagent pas les mêmes variables.
De fait, il n'est pas possible de faire hériter naivement BiDiCounter
des classes ForwardCounter
et BackwardCounter
puisque nous nous retrouvons avec le schéma d'héritage suivant :
La solution serait que ForwardCounter
et BackwardCounter
ne crée pas chacun une instance de BaseCounter
mais hérite de la même instance de BaseCounter
. Le schéma d'héritage deviendrait alors celui-ci :
Pour ce faire, les classes ForwardCounter
et BackwardCounter
vont faire référence à une même instance de la classe de base qui sera crée par l'objet qui héritera des classes ForwardCounter
et BackwardCounter
.
Pour ce faire, nous déclarons toujous la classe BaseCounter
comme précédemment. Par contre, nous mettons le mot-clé virtual
pour indiquer que la classe dérivant de la classe de base BaseCounter
fait référence à une classe unique qui peut-être partagée.
class ForwardCounter: public virtual BaseCounter
{
public:
void increment()
{
if(counter < max)
counter = counter + 1;
else
counter = 0;
}
ForwardCounter(): BaseCounter() {}
ForwardCounter(const ForwardCounter& aCounter): BaseCounter(aCounter) {}
explicit ForwardCounter(unsigned theMaxValue): ForwardCounter(0, theMaxValue) {}
ForwardCounter(unsigned theCounter, unsigned theMaxValue): BaseCounter(theCounter, theMaxValue) {}
};
class BackwardCounter: public virtual BaseCounter
{
public:
void decrement()
{
if(counter > 0)
counter = counter -1;
else
counter = max;
}
BackwardCounter(): BaseCounter() {}
BackwardCounter(const ForwardCounter& aCounter): BaseCounter(aCounter) {}
explicit BackwardCounter(unsigned theMaxValue): BackwardCounter(0, theMaxValue) {}
BackwardCounter(unsigned theCounter, unsigned theMaxValue): BaseCounter(theCounter, theMaxValue) {}
};
Apparemment, rien n'a changé par rapport au code qui a été donné pour la première solution proposée pour la question à l'exception du mot-clé virtual
. Cependant, le comportement est complètement différent.
Quand nous créons un objet class ForwardCounter: public BaseCounter
, je vais allouer une mémoire qui va correspondre à l'espace mémoire requis pour la classe BaseCounter
ainsi que la mémoire requise pour l'extension ForwardCounter
.
Quand nous créons un objet de type class ForwardCounter: public virtual BaseCounter
, la création dépend du fait que nous créons l'objet ForwardCounter
ou un objet qui dérive de ForwardCounter
.
Si l'objet créé a pour type
ForwardCounter
.
Considérons le code de création suivant :
ForwardCounter counter(0, 5);
le constructeur correspondant est :
ForwardCounter(unsigned theCounter, unsigned theMaxValue): BaseCounter(theCounter, theMaxValue) {}
la création de l'objet est décomposé en deux phases :
Allocation et initialisation d'un objet BaseCounter
qui est initialisé en appelant le constructeur tel que défini par ForwardCounter
, c'est à dire BaseCounter(0, 5)
,
Allocation et initialisation de l'objet ForwardCounter
qui contient un espace mémoire correspondant à la mémoire requise pour les données internes de ForwardCounter
(en l'espèce aucun champs n'est à allouer ni initialiser) ainsi qu'une référence à l'objet de type BaseCounter
qui a été précédemment créé.
Nous voyons bien que ForwardCounter
ne contient plus d'objet BaseCounter
en son sein mais fait référence à un objet BaseCounter
externe à l'objet ForwardCounter
.
Si l'objet créé a pour type un type dérivé comme par exemple
BiDiCounter
.
Considérons le code de création suivant :
BiDiCounter counter(5);
le constructeur correspondant est :
BiDiCounter(unsigned theCounter, unsigned theMaxValue):
ForwardCounter(), BackwardCounter(),
BaseCounter(theCounter, theMaxValue) {}
la création de l'objet est décomposé en trois phases :
Allocation et initialisation d'un objet BaseCounter
qui est initialisé en appelant le constructeur tel que défini par BiDiCounter
, c'est à dire BaseCounter(0, 5)
,
Allocation et initialisation de l'objet
BiDiCounter
qui contient une instance de
ForwardCounter
ainsi qu'une instance de
BackwardCounter
en allouant la mémoire nécessaire pour
ForwardCounter
,
BackwardCounter
ainsi que les définitions propres à
BiDiCounter
. Les constructeurs
ForwardCounter()
ainsi que
BackwardCounter()
.
Attention, lorsque le constructeur
ForwardCounter
(resp.
BackwardCounter
) est appellé par la classe dérivant de
ForwardCounter
(resp.
BackwardCounter
) pour initialiser que
ForwardCounter
(resp.
BackwardCounter
) et non pas la classe de base
BaseCounter
. La partie relative à
BaseCounter
dans la déclaration du constructeur
ForwardCounter(): BaseCounter() {}
est ignorée. Cette partie n“est active que quand on crée un objet de type ForwardCounter
(resp. BackwardCounter
) et non pas un objet dérivé de ForwardCounter
, (resp BackwardCounter
).
Affectation à chacun des instances de ForwardCounter
et de BackwardCounter
d'une référence à l'objet BaseCounter
qui a été créé.
Ce processus garantit que les instances ForwardCounter
et BackwardCounter
feront référence à une seule et unique instance BaseCounter
.
De ce fait, le code pour la classe BiDiCounter
devient le suivant :
class BiDiCounter: public ForwardCounter, public BackwardCounter
{
public:
BiDiCounter(): ForwardCounter(), BackwardCounter() {}
BiDiCounter(const BiDiCounter& aCounter):
ForwardCounter(aCounter),
BackwardCounter((const BackwardCounter&)aCounter),
BaseCounter(aCounter) {}
BiDiCounter(unsigned theMaxValue): BiDiCounter(0, theMaxValue) {}
BiDiCounter(unsigned theCounter, unsigned theMaxValue):
ForwardCounter(),
BackwardCounter(),
BaseCounter(theCounter, theMaxValue) {}
};
Ceci permet de comprendre que l'héritage multiple est intéressant conceptuellement, puisqu'il permet d'hériter de plusieurs comportements. Cependant, sa mise en oeuvre est relativement simple pour des cas où les classes dont on hérite ne dérivent pas d'une même classe de base. Si c'est le cas, il faut recourir à un héritage faisant référence aux classes qui est nettement moins intuitif et peut même conduire à des erreurs. En effet, il est nécessaire de créer l'instance qui sera partagée entre les différentes classes et à défaut de création explicite, s'il existe un constructeur par défaut, C++ utilisera ce constructeur par défaut pour initialiser l'objet partagé, même si ce n'était pas votre souhait mais simplement un oubli de votre part.