Comprendre le clonage

Si vous souhaitez un exemple de clonage avec C#, voir cet article.

Le clonage est la manière privilégiée pour dupliquer un objet polymorphique. Il s'agit donc d'une technique utile en C++, mais essentielle dans d'autres langages comme Java ou C# où le polymorphisme intervient plus souvent encore.

Pour illustrer le recours au clonage, imaginons l'exemple d'une classe Forme polymorphique, au sens où sa méthode dessiner() sur un flux puisse être spécialisée par les divers enfants de cette classe – les divers types de formes.

#include <ostream>
class Forme {
   char symb;
public:
   Forme(char symbole) : symb{ symbole } {
   }
   char symbole() const noexcept {
      return symb;
   }
   void symbole(char c) {
      symb = c;
   }
   virtual void dessiner(std::ostream &) const = 0;
   virtual ~Forme() = default;
};

Supposons aussi deux cas particuliers de Forme, symbolisés par les classes Carre et Triangle, les deux se dessinant chacun à sa manière.

class Carre : public Forme {
   int lar;
public:
   Carre(int lar) : Forme{ '#' }, lar{ lar } {
   }
   void dessiner(std::ostream &os) const override {
      for (int i = 0; i < lar; ++i) {
         for (int j = 0; j < lar; ++j)
            os << symbole();
         os << '\n';
      }
   }
};
class Triangle : public Forme {
   int haut;
public:
   Triangle(int haut) : Forme{ '*' }, haut{ haut } {
   }
   void dessiner(std::ostream &os) const override {
      for (int i = 0; i < haut; ++i) {
         for (int j = 0; j <= i; ++j)
            os << symbole();
         os << '\n';
      }
   }
};

Enfin, supposons un programme de test qui crée deux formes, l'une étant un carré et l'autre un triangle, puis les affiche toutes deux à la console.

Notez que le type unique_ptr est utilisé pour alléger l'écriture, et n'a pas de lien avec la problématique explorée ici.

#include <memory>
#include <iostream>
int main() {
   using namespace std;
   unique_ptr<Forme> f0{new Carre{3}};
   f0->dessiner(cout);
   unique_ptr<Forme> f1{new Triangle{5}};
   f1->dessiner(cout);
}

L'affichage obtenu en exécutant ce programme ira comme suit :

###
###
###
*
**
***
****
*****
Appuyez sur une touche pour continuer...

Jusqu'ici, pas besoin de clonage. Maintenant, imaginons que nous souhaitions dupliquer certaines de nos formes pour les afficher autrement (en utilisant un autre symbole que celui par défaut, disons) sans toutefois modifier la forme originale.

Par exemple, imaginons le programme de test proposé à droite, où la fonction afficher() prend en paramètre une indirection vers une sorte de Forme et un symbole, puis affiche un duplicat de la Forme reçue mais en utilisant le symbole choisi par le code client.

Nous ne voulons toutefois pas que les formes originales ne soient modifiées.

#include <memory>
#include <iostream>
int main() {
   using namespace std;
   unique_ptr<Forme> f0{new Carre{3}};
   f0->dessiner(cout);
   unique_ptr<Forme> f1{new Triangle{5}};
   f1->dessiner(cout);
   afficher(f0,'$');
   afficher(f1,'$');
   f0->dessiner(cout);
   f1->dessiner(cout);
}

Ainsi, l'affichage du programme de test à droite devrait être tel que proposé ci-dessous.

###
###
###
*
**
***
****
*****
$$$
$$$
$$$
$
$$
$$$
$$$$
$$$$$
###
###
###
*
**
***
****
*****
Appuyez sur une touche pour continuer...

On peut supposer un squelette pour la fonction afficher().

La fonction n'est pas en soi complexe, mais pour l'écrire correctement, il nous faut pouvoir dupliquer ce vers quoi mène le paramètre f, or qu'est-ce? Nous savons qu'il s'agit au moins d'une Forme, mais de quelle sorte de Forme s'agit-il? Que devrait-on indiquer là où le code à droite indique « /* ????? */ »?

#include <memory>
void afficher(const std::unique_ptr<Forme> &f, char c) {
   using namespace std;
   unique_ptr<Forme> aut_forme{ /* ????? */};
   aut_forme->symbole(c);
   aut_forme->dessiner(cout);
}

Voilà où le clonage devient nécessaire. Nous voulons que :

Étant donné que l'abstraction que nous avons choisi est Forme, il faut que le clonage soit possible sur une base polymorphique au moins à partir de cet endroit.

Il arrive assez fréquemment que les programmes qui ont recours au clonage définissent une interface Clonable pour encadrer cette pratique, mais ce n'est pas nécessaire ici.

#include <iostream>
class Forme {
   char symb;
protected:
   Forme(const Forme&) = default;
public:
   constexpr Forme(char symbole) : symb{symbole} {
   }
   constexpr char symbole() const noexcept {
      return symb;
   }
   constexpr void symbole(char c) {
      symb = c;
   }
   virtual void dessiner(std::ostream &) const = 0;
   virtual Forme* cloner() const = 0;
   virtual ~Forme() = default;
};

Le clonage s'implémente habituellement par un appel au constructeur de copie, dans la mesure où cet appel est réalisé par la fonction polymorphique cloner() elle-même. En effet, à cet endroit, le type effectif de l'objet à dupliquer est connu, et le constructeur de copie est un choix raisonnable.

Parce que les constructeurs de copie sont utiles au clonage mais seulement à l'interne (ils sont en général dangereux à utiliser de l'extérieur pour dupliquer des objets polymorphiques), on les qualifiera habituellement protected pour de tels objets. Notez que dans le cas qui nous intéresse, l'implémentation « par défaut » du constructeur de copie que le compilateur aurait généré pour nous est idéale... dans la mesure où les bons types sont impliqués! Pour cette raison, j'ai conservé les implémentations par défaut de ces méthodes, mais je les ai qualifiées protected.

Notez les types de retour covariants : la méthode virtuelle cloner() de Forme retourne un Forme* alors que la méthode virtuelle cloner() de Carre, enfant public de Forme, retourne un Carre*.

C'est légal et sain de procéder ainsi; en effet, si la méthode est appelée sur une Forme* mais que cette Forme est en fait un Carre, le fait de traiter le Carre* retourné comme un Forme* ne posera pas de problème car rien ne pourra être fait sur le Forme* qui n'aurait pas aussi pu se faire sur le Carre* réellement retourné.

class Carre : public Forme {
   int lar;
protected:
   Carre(const Carre&) = default;
public:
   Carre(int lar) : Forme{ '#' }, lar{ lar } {
   }
   void dessiner(std::ostream &os) const {
      for (int i = 0; i < lar; ++i) {
         for (int j = 0; j < lar; ++j)
            os << symbole();
         os << '\n';
      }
   }
   Carre* cloner() const {
      return new Carre{*this};
   }
};
class Triangle : public Forme {
   int haut;
protected:
   Triangle(const Triangle&) = default;
public:
   Triangle(int haut) : Forme{ '*' }, haut{ haut } {
   }
   void dessiner(std::ostream &os) const {
      for (int i = 0; i < haut_; ++i) {
         for (int j = 0; j <= i; ++j)
            os << symbole();
         os << '\n';
      }
   }
   Triangle* cloner() const {
      return new Triangle{*this};
   }
};

Pour dupliquer le paramètre f de la fonction afficher(), ne reste plus qu'à lui demander de se cloner lui-même et à opérer sur la copie résultante.

Remarquez l'utilité de faire en sorte que cloner() soit const : c'est en effet souvent lorsqu'on ne souhaite pas modifier l'objet original qu'on aura recours à une duplication.

#include <memory>
void afficher(const std::unique_ptr<Forme> &f, char c) {
   using namespace std;
   unique_ptr<Forme> aut_forme{f->cloner()};
   aut_forme->symbole(c);
   aut_forme->dessiner(cout);
}

Voilà, le tour est joué!

int main() {
   using namespace std;
   unique_ptr<Forme> f0{new Carre{3}};
   f0->dessiner(cout);
   unique_ptr<Forme> f1{new Triangle{5}};
   f1->dessiner(cout);
   afficher(f0, '$');
   afficher(f1, '$');
   f0->dessiner(cout);
   f1->dessiner(cout);
}

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !