User Tools

Site Tools


in204:tds:sujets:td2:part2

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.

Correction

Correction

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.

Correction

Correction

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.

Correction

Correction

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 ForwardCounterBaseCountercounter et ForwardCounterBaseCountermax
  • des fonctions getCounter, getMax, reset set, setMax qui vont modifier les champs précédents ForwardCounterBaseCountercounter et ForwardCounterBaseCountermax, nous allons identifier ces fonctions par leur chemin d'accès ForwardCounterBaseCountergetCounter, …
  • une fonctions increment qui va modifier le champ ForwardCounterBaseCountercounter.

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 BackwardCounterBaseCountercounter et BackwardCounterBaseCountermax
  • des fonctions getCounter, getMax, reset set, setMax qui vont modifier les champs précédents BackwardCounterBaseCountercounter et BackwardCounterBaseCountermax, nous allons identifier ces fonctions par leur chemin d'accès BackwardCounterBaseCountergetCounter, …
  • une fonctions decrement qui va modifier le champ BackwardCounterBaseCountercounter.

Ceci signifie que nous avons deux instances des champs max et counter en fonction de l'héritage :

  • soit ForwardCounterBaseCountermax ou BackwardCounterBaseCountermax,
  • soit ForwardCounterBaseCountercounter ou BackwardCounterBaseCountercounter.

Plus embêtant, la fonction increment modifie le champ ForwardCounterBaseCounter→>counter, la fonction decrement modifier le champ BackwardCounterBaseCounter→>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 :

    1. 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),
    2. 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 :

    1. 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),
    2. 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).

    3. 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.

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();
    }
}

Correction

Correction

Il suffit de tester les codes et de s'assurer du bon fonctionnement.

in204/tds/sujets/td2/part2.txt · Last modified: 2022/11/18 10:49 (external edit)