Code de grande personne – pointeurs de méthodes d'instance

Imaginons une usine fictive à laquelle il est possible d'envoyer des commandes représentées par des entiers sur huit bits de valeur 0 (signal pour faire monter une plateforme), 1 (pour la faire descendre), 2 (pour émettre de la fumée) et 3 (pour faire vibrer la plateforme).

Imaginons aussi la classe suivante dont le rôle sera de simuler l'action de cette usine :

#include <iostream>
#include <cassert>
using namespace std;
class PetiteUsine
{
   void monter()
      { cout << "Je monte!" << endl; }
   void descendre()
      { cout << "Je descend!" << endl; }
   void boucaner()
      { cout << "Je boucane!" << endl; }
   void vibrer()
      { cout << "Je vibre!" << endl; }
public:
   enum class Code : char
   {
      MONTER = 0x00, DESCENDRE = 0x01, BOUCANER = 0x02, VIBRER = 0x03
   };
   void agir(Code c)
   {
      switch(c)
      {
      case Code::MONTER:
         monter();
         break;
      case Code::DESCENDRE:
         descendre();
         break;
      case Code::BOUCANER:
         boucaner();
         break;
      case Code::VIBRER:
         vibrer();
         break;
      default:
         assert(false && "ERREUR GRAVE");
      }
   }
};
//
// Petit test tout simple
//
int main()
{
   PetiteUsine miniU;
   miniU.agir(PetiteUsine::Code::BOUCANER);
}

Remarquez que l'opération agir(const Code) ne se prête pas a priori à une action polymorphique puisqu'elle dépend d'un signal décrit par une constante entière. La sélective est un palliatif raisonnable à ce problème et permet de déterminer dans un temps raisonnable la méthode la plus appropriée à appeler. Cela dit, cette solution n'est pas optimale du point de vue performance.

On peut faire beaucoup mieux.

Solution 00 – polymorphisme et objets d'action

Une solution possible exploite le polymorphisme et des classes internes (en fait, que les classes de soutien soient internes rend la solution plus élégante mais n'est pas fondamental à la solution).

L'idée va comme suit :

Le code résultant serait :

#include <iostream>
#include <memory>
using namespace std;
class PetiteUsine
{
   struct Acteur
   {
      virtual void agir() = 0;
      virtual ~Acteur() = default;
   };
   struct Monteur : Acteur
   {
      void agir() override
         { cout << "Je monte!" << endl; }
   };
   struct Descendeur : Acteur
   {
      void agir() override
         { cout << "Je descend!" << endl; }
   };
   struct Boucaneur : Acteur
   {
      void agir() override
         { cout << "Je boucane!" << endl; }
   };
   struct Vibreur : Acteur
   {
      void agir() override
         { cout << "Je vibre!" << endl; }
   };
public:
   enum class Code : char
   {
      MONTER = 0x00, DESCENDRE = 0x01, BOUCANER = 0x02, VIBRER = 0x03,
      NB_CODES // sentinelle
   };
private:
   unique_ptr<Acteur> acteurs_[static_cast<int>(Code::NB_CODES)];
public:
   PetiteUsine()
   {
      acteurs_[Code::MONTER] = make_unique<Monteur>();
      acteurs_[Code::DESCENDRE] = make_unique<Descendeur>();
      acteurs_[Code::BOUCANER] = make_unique<Boucaneur>();
      acteurs_[Code::VIBRER] = make_unique<Vibreur>();
   }
   void agir(Code c)
   {
      // Valider que c >= 0 && c < NB_CODES au besoin
      acteurs_[c]->agir();
   }
};
int main()
{
   PetiteUsine miniU;
   miniU.agir(PetiteUsine::Code::BOUCANER);
}

Remarquez que le programme principal n'a pas changé d'un iota, ce qui est une vertu. Remarquez aussi que le recours à une indirection polymorphique entraîne un léger coût initial et final pour instancier et détruire dynamiquement les objets représentant les actions à prendre. Ce coût est nettement amorti en situation réelle par le gain de performance en cours d'exécution.

Solution 01 – polymorphisme et objets d'action indirects

S'il est important que ce soit les méthodes de PetiteUsine qui soient invoquées, on pourrait légèrement compliquer la sauce en informant chaque objet d'action, dès sa construction, de l'existence de l'instance de PetiteUsine à laquelle il réfère et de faire en sorte qu'il invoque la méthode appropriée de la PetiteUsine :

#include <iostream>
#include <memory>
using namespace std;
class PetiteUsine
{
   void monter()
      { cout << "Je monte!" << endl; }
   void descendre()
      { cout << "Je descend!" << endl; }
   void boucaner()
      { cout << "Je boucane!" << endl; }
   void vibrer()
      { cout << "Je vibre!" << endl; }
   class Acteur
   {
      PetiteUsine &p_;
   public:
      Acteur(PetiteUsine &p) noexcept
         : p_{p}
      {
      }
      PetiteUsine& petite_usine() noexcept
         { return p_; }
      virtual void agir() = 0;
      virtual ~Acteur() = default;
   };
   struct Monteur : Acteur
   {
      Monteur(PetiteUsine &p) noexcept
         : Acteur{p}
      {
      }
      void agir()
         { petite_usine().monter(); }
   };
   struct Descendeur : Acteur
   {
      Descendeur(PetiteUsine &p) noexcept
         : Acteur{p}
      {
      }
      void agir()
         { petite_usine().descendre(); }
   };
   struct Boucaneur : Acteur
   {
      Boucaneur(PetiteUsine &p) noexcept
         : Acteur{p}
      {
      }
      void agir()
         { petite_usine().boucaner(); }
   };
   struct Vibreur : Acteur
   {
      Vibreur(PetiteUsine &p) noexcept
         : Acteur{p}
      {
      }
      void agir()
         { petite_usine().vibrer(); }
   };
public:
   enum class Code : char
   {
      MONTER = 0x00, DESCENDRE = 0x01, BOUCANER = 0x02, VIBRER = 0x03,
      NB_CODES // sentinelle
   };
private:
   unique_ptr<Acteur> acteurs_[static_cast<int>(Code::NB_CODES)];
public:
   PetiteUsine() // prudence: pas exception-safe
   {
      acteurs_[Code::MONTER] = make_unique<Monteur>(*this);
      acteurs_[Code::DESCENDRE] = make_unique<Descendeur>(*this);
      acteurs_[Code::BOUCANER] = make_unique<Boucaneur>(*this);
      acteurs_[Code::VIBRER] = make_unique<Vibreur>(*this);
   }
   void agir(Code c)
   {
      // Valider que c >= 0 && c < NB_CODES au besoin
      acteurs_[c]->agir();
   }
};
int main()
{
   PetiteUsine miniU;
   miniU.agir(PetiteUsine::Code::BOUCANER);
}

Solution 02 – polymorphisme et objets d'action indirects (suite)

Une alternative légèrement plus simple mais légèrement plus lente serait d'alléger un peu le tout et passer la PetiteUsine en paramètre à la méthode agir() des objets d'action :

#include <iostream>
#include <memory>
using namespace std;
class PetiteUsine
{
   void monter()
      { cout << "Je monte!" << endl; }
   void descendre()
      { cout << "Je descend!" << endl; }
   void boucaner()
      { cout << "Je boucane!" << endl; }
   void vibrer()
      { cout << "Je vibre!" << endl; }
   struct Acteur
   {
      virtual void agir(PetiteUsine&) = 0;
      virtual ~Acteur() = default;
   };
   struct Monteur : Acteur
   {
      void agir(PetiteUsine &p) override
         { p->monter(); }
   };
   struct Descendeur : Acteur
   {
      void agir(PetiteUsine &p) override
         { p->descendre(); }
   };
   struct Boucaneur : Acteur
   {
      void agir(PetiteUsine &p) override
         { p->boucaner(); }
   };
   struct Vibreur : Acteur
   {
      void agir(PetiteUsine &p) override
         { p->vibrer(); }
   };
public:
   enum class Code : char
   {
      MONTER = 0x00, DESCENDRE = 0x01, BOUCANER = 0x02, VIBRER = 0x03,
      NB_CODES // sentinelle
   };
private:
   unique_ptr<Acteur> acteurs_[static_cast<int>(Code::NB_CODES)];
public:
   PetiteUsine()
   {
      acteurs_[Code::MONTER] = make_unique<Monteur>();
      acteurs_[Code::DESCENDRE] = make_unique<Descendeur>();
      acteurs_[Code::BOUCANER] = make_unique<Boucaneur>();
      acteurs_[Code::VIBRER] = make_unique<Vibreur>();
   }
   void agir(Code c)
   {
      // Valider que c >= 0 && c < NB_CODES au besoin
      acteurs_[c]->agir(*this);
   }
};
int main()
{
   PetiteUsine miniU;
   miniU.agir(PetiteUsine::Code::BOUCANER);
}

Solution 03 – pointeurs de méthodes d'instance

La solution ayant recours à une racine polymorphique et à des spécialistes dérivés est à la fois puissante, flexible et efficace. Cela dit, dans un cas comme le nôtre, on pourrait faire légèrement plus compact (et plus simple) en remarquant ce qui suit :

La stratégie reposant sur l'utilisation d'un tableau indicé par les codes d'action peut être recyclée ici dans un contexte plus simple. On peut en effet utiliser dans une PetiteUsine un tableau de pointeurs de méthodes d'instance dans lequel chaque indice correspond à la méthode d'action appropriée.

Le code ressemblerait alors à ceci :

#include <iostream>
using namespace std;
class PetiteUsine
{
   void monter()
      { cout << "Je monte!" << endl; }
   void descendre()
      { cout << "Je descend!" << endl; }
   void boucaner()
      { cout << "Je boucane!" << endl; }
   void vibrer()
      { cout << "Je vibre!" << endl; }
   // type de ces méthodes
   using methode_action = void (PetiteUsine::*)();
public:
   enum class Code : char
   {
      MONTER = 0x00, DESCENDRE = 0x01, BOUCANER = 0x02, VIBRER = 0x03,
      NB_CODES // sentinelle
   };
private:
   methode_action action_[static_cast<int>(Code::NB_CODES)];
public:
   PetiteUsine() noexcept
   {
      // Capture des pointeurs sur les méthodes
      action_[Code::MONTER] = &PetiteUsine::monter;
      action_[Code::DESCENDRE] = &PetiteUsine::descendre;
      action_[Code::BOUCANER] = &PetiteUsine::boucaner;
      action_[Code::VIBRER] = &PetiteUsine::vibrer;
   }
   void agir(const Code c)
   {
      // Valider que c >= 0 && c < NB_CODES au besoin
      (this->*action_[c])();
   }
};
int main()
{
   PetiteUsine miniU;
   miniU.agir(PetiteUsine::Code::BOUCANER);
}

Le type interne methode_action dont la signature est un peu étrange indique que methode_action représente l'adresse d'une méthode d'instance de PetiteUsine, méthode ne prenant pas de paramètre et ne retournant rien.

L'attribut d'instance action_ est un tableau de methode_action auquel on affecte, aux indices appropriés, les adresses des méthodes à invoquer pour chaque code d'action. On pourrait couvrir les indices auxquels ne correspond pas de code (s'il y en avait) avec des pointeurs nuls et valider le pointeur de méthode avant l'invocation.

L'invocation d'une méthode d'instance à travers un pointeur utilise un opérateur peu connu de C++ : l'opérateur ->*. La syntaxe devrait être claire à partir de l'exemple visible dans PetiteUsine::agir().

Cette approche est plus limitée de potentiel que les précédentes qui reposaient sur des objets d'action (avec constructeurs, attributs et toute la flexibilité et la puissance du modèle OO) mais est plus simple et ne demande pas d'allocation ou de libération dynamique de mémoire.

Pointeurs de méthodes d'instances – quelques détails techniques

Les pointeurs de méthodes d'instance sont des entités particulières sur le plan de la syntaxe. Toutefois, notez :

Un exemple d'appel indirect de méthodes const et polymorphiques suit, pour illustrer le tout. Notez que cet exemple est académique, mais qu'il a pour but de vous aider à apprivoiser la syntaxe, sans plus. Merci à Kenzo Lespagnol et à Mauricio Medina de la cohorte 07 du Diplôme de développement du jeu vidéo à l'Université de Sherbrooke pour avoir soulevé la question.

#include <iostream>
using namespace std;
struct X
{
   virtual void afficher(ostream &os) const
      { os << "X" << endl; }
   virtual ~X() = default;
};
struct Y : X
{
   void afficher(ostream &os) const
      { os << "Y" << endl; }
};
struct Z : X
{
   void afficher(ostream &os) const
      { os << "Z" << endl; }
};
void afficher(const X &x, ostream &os)
{
   void (X::*f)(ostream &) const = &X::afficher;
   (x.*f)(os);
}
int main()
{
   X x;
   Y y;
   Z z;
   afficher(x, cout);
   afficher(y, cout);
   afficher(z, cout);
}

La tout affichera candidement ceci :

X
Y
Z

Voilà!


Valid XHTML 1.0 Transitional

CSS Valide !