Des union étiquetés

La technique décrite ici était surtout utile avant que la POO ne nous permette facilement de mettre en application le polymorphisme dynamique; son utilité aujourd'hui se trouve surtout cantonnée dans l'implémentation des types tels que le VARIANT de Visual Basic ou les paquets transmis par UDP.

On peut considérer les union étiquetés (an anglais, Discriminated union) comme une forme un peu primitive d'effacement de types. En ce sens, ils constituent un mécanisme pour entreposer en un même lieu des données de divers types, et il est généralement plus simple de déposer des valeurs dans une telle structure de données que de les en extraire.

L'idée

L'idée derrière les union étiquetés est de permettre de constituer une sorte d'enregistrement qui puisse contenir plusieurs représentations distinctes, mais de le munir d'une valeur (l'étiquette) permettant d'indiquer laquelle des sortes possibles cet enregistrement contient.

Le consommateur d'une telle entité examinera l'étiquette (probablement dans une sélective), puis posera les gestes requis pour consommer les données en fonction de la sorte qui aura été constatée. C'est une forme d'antipolymorphisme, donc une pratique à éviter (dans du code contemporain, du moins), mais qui est utille dans des créneaux pointus comme la sérialisation de données.

Les union

On utilise peu les union aujourd'hui, car ils ont plusieurs irritants déplaisants; en particulier, leur représentation binaire dépend de la plateforme, ce qui mène souvent à du code non-portable. En gros :

Un union est une sorte d'agrégat dont tous les états débutent à la même adresse. Conséquemment, écrire dans l'un des états écrit aussi dans les autres, ceux-ci étant (au moins en partie) superposés. C'est en partie ce qui le mène à avoir des représentations non-portables : dans un exemple comme celui proposé à droite, puisque l'ordre des bytes dans un entier varie selon la plateforme, bytes[0] ne correspondra pas nécessairement au même byte de dword sur toutes les plateformes possibles.

Si la taille d'un union est la taille de son attribut le plus gros, la taille d'un struct ou d'une class, à titre comparatif, est proportionnelle à la somme de la taille de ses attributs.

#include <cstdint>
#include <cassert>
using namespace std;
union registre_32_bits
{
   int8_t bytes[4];
   int16_t words[2];
   int32_t dword;
};
int main()
{
   static_assert(sizeof(registre_32_bits) == sizeof(int32_t), "Hum...");
}

Avec C++ 11, un union peut implémenter quelques méthodes « spéciales », en particulier celles qui composent la Sainte-Trinité et celles qui permettent la sémantique de mouvement.

Un union ne peut toutefois participer à une relation d'héritage, et ne peut conséquemment pas avoir de méthodes virtuelles.

#include <algorithm>
template <class T>
   union raw_view
   {
      T obj;
      char raw[sizeof(T)];
      registre_32_bits()
      {
         using namespace std;
         fill(begin(raw), end(raw), 0);
      }
   };

Avec C++ 11, un union peut aussi avoir des instances de classes complexes, par exemple des std::string, en tant qu'attributs. Cependant, si les fonctions « spéciales » de ces attributs ne sont pas triviales, elles seront implicitement supprimées. En effet, si un union devait contenir au moins deux attributs dont les constructeurs, le destructeur ou l'affectation ne sont pas triviaux, il ne serait pas possible de déterminer les fonctions duquel devraient être privilégiées.

Un union ne peut avoir des attributs qui soient des références, mais a droit aux membres static.

Implémentation typique d'un union étiqueté

Supposons un jeu en réseau très simple sur une carte 2D de cases entières, devant envoyer trois types de messages :

Les messages pourraient être structurés comme suit :

#include <algorithm>
//
// Types auxiliaires
//
using id_type = int;
struct position
{
   int x, y;
   position()
      : x{}, y{}
   {
   }
   position(int x, int y)
      : x{x}, y{y}
   {
   }
   // etc.
};
struct nom_joueur
{
   static constexpr const std::size_t CAPACITE = 32;
   char texte[CAPACITE];
   nom_joueur()
   {
      using namespace std;
      fill(begin(texte), end(texte), 0);
   }
   nom_joueur(const char *src)
   {
      using namespace std;
      auto sz = min(CAPACITE, strlen(src));
      copy(src, src + sz, texte);
      fill(begin(texte) + sz, end(texte), 0);
   }
   // ...
};
//
// Les messages en soi
//
struct deplacement
{
   id_type id;
   position depart,
            arrivee;
   deplacement(id_type id, const position &depart, const position &arrivee)
      : id{ id }, depart{ depart }, arrivee{ arrivee }
   {
   }
   // ...
};
struct attaque
{
   id_type agresseur,
           cible;
   float degats;
   // ...
};
struct missive
{
   static constexpr const std::size_t CAPACITE = 128;
   nom_joueur de_qui,
              a_qui;
   char message[CAPACITE];
   // ...
};

Un message constitué d'un union étiqueté pourrait alors être :

struct message
{
   enum class type : short
   {
      Missive, Attaque, Deplacement
   };
   type sorte;
   union
   {
      missive missive_;
      attaque attaque_;
      deplacement deplacement_;
   };
   message(const missive &m)
      : sorte{ type::Missive }, missive_(m)
   {
   }
   message(const attaque &a)
      : sorte{ type::Attaque }, attaque_(a)
   {
   }
   message(const deplacement &d)
      : sorte{ type::Deplacement }, deplacement_{ d }
   {
   }
};

L'étiquette d'un message ici sera son attribut sorte. J'ai utilisé le type du paramètre de chacun des constructeurs de message pour déterminer automatiquement la valeur de sorte, mais ce n'est qu'une pratique parmi plusieurs. J'ai aussi utilisé un union anonyme, pour que les attributs de l'union soient « aplatis » dans message et alléger ainsi quelque peu la syntaxe pour leur accéder, mais ici encore, c'est une façon de faire parmi plusieurs, sans plus.

Un exemple d'utilisation serait :

#include <iostream>
using namespace std;
ostream& operator<<(ostream &os, const position &pos)
{
   return os << pos.x << ',' << pos.y;
}
int main()
{
   // Creation d'un message de deplacement
   auto msg = message{deplacement{3, position{1,1}, position{2,2}}};
   // ...
   //
   // decodage d'un message
   //
   switch (msg.sorte)
   {
   case message::type::Attaque:
      // ...
      break;
   case message::type::Deplacement:
      cout << "L'entite " << msg.deplacement_.id
           << " va de " << msg.deplacement_.depart
           << " vers " << msg.deplacement_.arrivee << endl;
      break;
   case message::type::Missive:
      // ...
      break;
   }
}

Évidemment, en pratique, on ajoutera à ce type de code une gamme de services pour faciliter la manipulation des messages. J'ai été quelque peu minimaliste dans ma démarche pour les fins de cet exemple.

Lectures supplémentaires

Quelques liens suivent, pour enrichir votre compréhension.


Valid XHTML 1.0 Transitional

CSS Valide !