This is an old revision of the document!
Partie II – Constructeurs & Arbres de Dérivation
TD2
Nous reprenons le code C++ de la partie précédente.
Question n°1
Ajoutez à la classe MyBiDiCounter l’ensemble des constructeurs dont notamment :
le constructeur par défaut,
le constructeur de recopie,
le constructeur spécifiant la valeur maximale,
le constructeur spécifiant à la fois la valeur courante du compteur et la valeur maximale.
Modifier le code de la fonction testMyBiDiCounter
pour appeler le bon constructeur.
Sauvegarder votre projet pour pouvoir le réalisation dans la partie III du TD.
Dans la suite, nous vous conseillons de définir un nouveau projet utilisant une nouvelle copie des fichiers que vous avez réalisé, puisque nous allons partir de ce code à la fois dans les questions suivants mais aussi dans la partie III du TD.
Le code consolidé de la classe MyBiDiCounter
est le suivant:
class MyBiDiCounter: public MyCounter
{
public:
MyBiDiCounter(): MyCounter() {}
MyBiDiCounter(const MyBiDiCounter& anotherCounter):
MyCounter(anotherCounter) {}
explicit MyBiDiCounter(uint theMaxValue): MyCounter(theMaxValue) {}
MyBiDiCounter(uint theCounterValue, uint theMaxValue):
MyCounter(theCounterValue, theMaxValue) {}
void decrement()
{
if(counter > 0)
counter --;
else
counter = max;
}
void print() const
{
std::cout << "Compteur: " << counter << "/" << max << std::endl;
}
};
Ceci permet de simplifier le code de la fonction testMyBiDiCounter
en supprimant les appels aux fonctions setMax
et reset
.
void testMyBiDiCounter()
{
MyBiDiCounter counterA(4);
counterA.print();
for(int i=0; i < 6; i++)
{
counterA.increment();
counterA.print();
}
for(int i=0; i < 6; i++)
{
counterA.decrement();
counterA.print();
}
}
Question n°2
La classe MyBiDiCounter
ajoute la fonction decrement
à la classe MyCounter
.
En fait, nous pouvons définir une famille de compteur :
Le compteur ForwardCounter
qui compte de 0 à max et repars à 0.
Le compteur BackwardCounter
qui compte max à 9 et repars à max.
Et le compteur BiDiCounter
qui peut incrémenter ou décrémenter le compteur interne.
Nous souhaitons partager le maximum de code entre ces différents compteurs. Une solution consiste à définir l’arbre de dérivation suivant :
et nous souhaitons factoriser le maximum de code entre les classes ForwardCounter
, ReverseCounter
& BiDiCounter
, l’objectif étant que ces trois classes contiennent le minimum de code.
Question n°2.1
Faites la liste des méthodes, champs pouvant être partagés et la liste des méthodes et champs propres à chacune des classes.
L'ensemble des méthodes de MyCounter
doivent être présentes dans la classes BaseCounter
à l'exception de la méthode increment
.
En effet, ces méthodes sont communes aux trois classes ForwardCounter
, ReverseCounter
et BiDiCounter
.
Question n°2.2
Implanter la classe BaseCounter
. On s’inspirera fortement de la classe MyCounter
déjà définie.
Une proposition pour la classes BaseCounter
serait la classe suivante :
#ifndef COUNTER_HPP
#define COUNTER_HPP
#include<iostream>
class BaseCounter
{
protected:
unsigned counter;
unsigned max;
public:
unsigned getCounter() const { return counter; }
unsigned getMax() const { return max; }
void reset() { counter = 0; }
void set(unsigned value) { counter = value; }
void setMax(unsigned value)
{
max = value;
if(value > counter)
counter = counter % max;
}
protected:
BaseCounter(): counter(0), max(0) {}
BaseCounterunsigned theCounter,
unsigned theMax): counter(theCounter), max(theMax)
{}
BaseCounterunsigned (const BaseCounterunsigned & anotherCounter):
counter(anotherCounter.counter),
max(anotherCounter.max)
{}
~BaseCounterunsigned ()
{}
};
Nous supposons que la classe BaseCounter
n'est qu'une pure classe de base, c'est-à-dire qu'aucun objet de type BaseCounter
sera créé. Pour éviter de pouvoir créer un objet de type BaseCounter
, nous modifions la visibilité des constructeurs de constructeurs public
en constructeur protected
. Désormais, les constructeurs ne pourront être appelés que par des classes qui dérivent de la classe BaseCounter
, interdisant ainsi la possibilité de créer accidentellement un objet de type BaseCounter
.
Question n°2.3
Implanter les classes ForwardCounter
, BackwardCounter
et BiDiCounter
qui héritent chacune de la classe BaseCounter
.
Correction 1 - Héritage Simple
Correction 1 - Héritage Simple
Cette correction propose de créer trois classes ForwardCounter
, BackwardCounter
et BiDiCounter
qui héritent de la classe BaseCounter
en l'étendant avec les fonctions manquantes :
increment
pour la classe ForwardCounter
,
decrement
pour la classe BackwardCounter
,
increment
et decrement
pour la classe BiDiCounter
.
Le code correspondant à cette solution est le suivant :
class ForwardCounter: public 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 BaseCounter
{
public:
void decrement()
{
if(counter > 0)
counter = counter -1;
else
counter = max;
}
BackwardCounter(): BaseCounter() {}
BackwardCounter(const BackwardCounter& aCounter): BaseCounter(aCounter) {}
explicit BackwardCounter(unsigned theMaxValue): BackwardCounter(0, theMaxValue) {}
BackwardCounter(unsigned theCounter, unsigned theMaxValue): BaseCounter(theCounter, theMaxValue) {}
};
class BiDiCounter: public BaseCounter
{
public:
void increment()
{
if(counter < max)
counter = counter + 1;
else
counter = 0;
}
void decrement()
{
if(counter > 0)
counter = counter -1;
else
counter = max;
}
BiDiCounter(): BaseCounter() {}
BiDiCounter(const BiDiCounter& aCounter): BaseCounter(aCounter) {}
explicit BiDiCounter(unsigned theMaxValue): ForwardCounter(0, theMaxValue) {}
BiDiCounter(unsigned theCounter, unsigned theMaxValue): BaseCounter(theCounter, theMaxValue) {}
};
Correction 2 - Héritage Simple en factorisant l'implantation des fonctions increment
et decrement
Correction 2 - Héritage Simple en factorisant l'implantation des fonctions increment
et decrement
Dans l'exemple précédent, nous constatons que nous définissons deux fois le code pour la fonction increment
et la fonction decrement
, ce qui conduit à dupliquer le code et surtout à devoir si jamais nous trouvons une erreur dans une des fonctions membres increment
(resp. decrement
) de devoir penser à corriger la deuxième implantation de la fonction membre increment
(resp. decrement
).
Dans ce cas, il est possible d'implanter les deux fonctions __increment
et __decrement
qui sont des fonctions internes à la classe et accessibles uniquement des classes dérivées. Ces fonctions contiennent le code de increment
et de decrement
, nous les avons préfixés par __
pour bien indiquer qu'il s'agit de fonction interne. Elles seront donc déclarées comme protected
dans la classe BaseCounter
qui désormais s'écrira comme suit :
class BaseCounter
{
protected:
unsigned counter;
unsigned max;
public:
unsigned getCounter() const { return counter; }
unsigned getMax() const { return max; }
void reset() { counter = 0; }
void set(unsigned value) { counter = value; }
void setMax(unsigned value)
{
max = value;
if(value > counter)
counter = counter % max;
}
protected:
BaseCounter(): counter(0), max(0) {}
BaseCounterunsigned theCounter,
unsigned theMax): counter(theCounter), max(theMax)
{}
BaseCounter (const BaseCounterunsigned & anotherCounter):
counter(anotherCounter.counter),
max(anotherCounter.max)
{}
~BaseCounterunsigned ()
{}
void __increment()
{
if(counter < max)
counter = counter + 1;
else
counter = 0;
}
void __decrement()
{
if(counter > 0)
counter = counter -1;
else
counter = max;
}
};
Il suffit désormais d'ajouter aux classes ForwardCounter
et BiDiCounter
la fonction :
public:
void increment() { __increment(); }
ainsi qu'aux classes BackwardCounter
et BiDiCounter
la fonction ::
public:
void decrement() { __decrement(); }
Ce qui nous donne le code suivant pour les trois classes dérivées :
class ForwardCounter: public BaseCounter
{
public:
void increment() { __increment(); }
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 BaseCounter
{
public:
void decrement() { __decrement(); }
BackwardCounter(): BaseCounter() {}
BackwardCounter(const BackwardCounter& aCounter): BaseCounter(aCounter) {}
explicit BackwardCounter(unsigned theMaxValue): BackwardCounter(0, theMaxValue) {}
BackwardCounter(unsigned theCounter, unsigned theMaxValue): BaseCounter(theCounter, theMaxValue) {}
};
class BiDiCounter: public BaseCounter
{
public:
void increment() { __increment(); }
void decrement() { __decrement(); }
BiDiCounter(): BaseCounter() {}
BiDiCounter(const BiDiCounter& aCounter): BaseCounter(aCounter) {}
explicit BiDiCounter(unsigned theMaxValue): ForwardCounter(0, theMaxValue) {}
BiDiCounter(unsigned theCounter, unsigned theMaxValue): BaseCounter(theCounter, theMaxValue) {}
};
Correction 3 - Héritage Multiples
Correction 3 - Héritage Multiples
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
.
Considérons le code de création suivant :
<code cpp>
ForwardCounter counter(0, 5);
</code>
le constructeur correspondant est :
<code cpp>
ForwardCounter(unsigned theCounter, unsigned theMaxValue): BaseCounter(theCounter, theMaxValue) {}
</code>
la création de l'objet est décomposé en deux phases :
- Allocation d'un objet ''BaseCounter'' qui est initialisé en appelant le constructeur tel que
défini par ''ForwardCounter'', c'est à dire ''BaseCounter(0, 5)'',
- Allocation 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''.
Considérons le code de création suivant :
<code cpp>
BiDiCounter counter(5);
</code>
le constructeur correspondant est :
<code cpp>
BiDiCounter(unsigned theCounter, unsigned theMaxValue):
ForwardCounter(theCounter, theMaxValue),
BaseCounter(theCounter, theMaxValue) {}
</code>
Question n°3
Tester le comportement de vos compteurs à partir du code suivant
void testFamilyOfCounters()
{
ForwardCounter incCounter(0, 4);
BackwardCounter decCounter(0, 3);
BiDiCounter biDiCounter(0, 5);
for(int i=0; i < 6; i++)
{
incCounter.increment();
incCounter.print();
decCounter.decrement();
decCounter.print();
biDiCounter.increment();
biDiCounter.print();
}
for(int i=0; i < 6; i++)
{
biDiCounter.decrement();
biDiCounter.print();
}
}