Union

Introduction

Cette article présente un ensemble de classes C++ permettant de simuler de l'héritage de classes virtuelles (virtuelle sous-entend que la classe n'existe pas, on pourait parler d'h&ecute;ritage de classes fictives). Il est né √† la suite d'une discussion avec mon ami damien. Le terme Union n'est pas aà prendre au sens C/C++, un autre nom serait surement plus approprié mais je ne l'ai pas sous la main ;).

Le Problème

Prenons le code suivant :

listing 1 [+/-]

La fonction func effectue un traitement sur Foo en utilisant que les methodes offertes par A et B (a_func et b_func). En partant de ce constat, un objet de type Bar qui hérite également de A et B est alors compatible pour func. La premiere solution consiste a modifier le prototype de func et d'insérer une nouvelle classe AB héritant de A et b. On peut alors passer à func un paramètre de type AB et il ne reste finalement qu'à faire hériter Foo et Bar de cette nouvelle classe (listing 2).

listing 2 [+/-]

Cette première solution ne fait que repousser le problème et l'ajout d'une nouvelle interface D et de fonctions voulant des objets de type "AB", "ABD", etc... rend le code difficilement maintenable et oblige a changer le graphe d'héritage continuellement.
Une seconde solution consiste a utiliser le design pattern des fonctions template pour avoir au final une fonction du type :

listing 3 [+/-]

Cette approche utilise le principe du : "Qui peut le plus, peut le moins". En effet, on peut maintenant passer tout objet heritant publiquement de A et B mais également tout ceux possédant des méthodes utilisées dans la fonction (ici a_func et b_func). On perd alors tout controle sémantique.

Enfin un dernier point qui nous fait écarter la solution précédente reste le stockage dans un conteneur d'objets héritant de A et B. C'est ce problème que nous allons tenter de résoudre dans la suite de l'article.

Proposition

La solution recherchée consiste a simuler l'existance d'une classe AB dont toutes les classes héritant de A et B hériterait.

listing 4 [+/-]

Pour simuler le type d'objet, l'idée consiste à créer une classe conteneur donnant accès aux interfaces A et B d'un objet. Cette classe ressemble à :

listing 5 [+/-]

Tout objet castable en A et B peut maintenant √™tre passé en paramètre de la fonction func_AB. Deplus il suffit d'imlémenter les operateurs de copie et d'affectation sur la classe AB_Compatible pour pouvoir stocker des objets dans un vector ou un map.
Maintenant que le principe est adopté il reste a rendre la classe plus générique pour pouvoir supporter une union d'un nombre quelquonque de classes. Pour cette partie, nous nous servons du principe des typelist. Les typelist vont nous permettre de définir l'ensemble des interfaces nécéssaires, dans chaque noeud, nous conservons un pointeur vers l'objet de base casté dans l'interface supporté par le Noeud. Chaque noeud implemente également un opérateur de cast vers l'interface supporté, ceci va permettre de récupérer à partir de l'union de facon transparente

listing 5b [+/-]

La déclaration d'union se fait donc de manière récursive en chainant les unions. Pour reprendre la fonction précedente, nous obtenons :

listing 6 [+/-]
les operateur de convertion en T* et T& sont red√©finis dans la classe Union. Cette approche permet de pouvoir récupérer les interfaces par pointeur ou référence à partir d'un objet Union. Par exemple si une interface offfre un operateur [] travailler avec une référence reste plus naturelle (interface_tab[i] au lieu de (*interface_tab)[i]).

Pour simplifier l'écriture du typedef, nous pouvons utiliser l'une des méthodes présentées par Andrei Alexandrescu. Celle consiste à définir une classe templatée par n paramètres définissant un typedef de l'union de ces n parametres. La spécialisation partielle est ensuite utilisée pour pouvoir utiliser la classe avec p parametre (p <=n). Cette méthode illustrée par le listing 7 peut √™tre générée, dans son article sur les typelist, Andrei génère comme ceci des typelist de taille 50, nous nous arreterons ici à 8.

listing 7 [+/-]

Il suffit alors d'utiliser Union<A, B>::type comme type en paramètre de la fonction func_AB. Pour terminer l'implémentation de la classe UnionT, nous pouvons ajouter des constructeurs des opérateur de convertion const et non-const. De cette facon, une fonction prenant en paramètre un const Union ne pourra accéder qu'aux méthodes const dans les interfaces de l'union.

Application

L'exemple d'application repose sur des algorithme pouvant s'appliquer a un mesh 3D. Un mesh est un ensemble de tableau contenant position de sommet, coordonnées de textures, coleur au sommet normales, etc... Certain traitement peuvent s'appliquer de manière générique à la condition que certaine interfaces soient présentes. Les Union peuvent donc s'appliquer dans le cas présent. Le code suivant défini les interfaces accesseur de normales, couleurs et sommets, suivi d'un classe mesh contenant ces informations(i.e. héritant des interfaces) :

listing 8 [+/-]

La classe et les interfaces étant définies, voici les trois algorithmes applicables au mesh : la normalisation des normales, l'affichage du nuage de points en couleur et le grossissement du mesh:

listing 9 [+/-]

Pour finir, voici l'utilisation de ce qui a été mis en place.

listing 10 [+/-]

Conclusion

Nous avons au travers de cet article montré une méthode originale pour permettre de définir des algorithmes génériques sur des objets implémentant un ensemble défini d'interface. L'utilisation des unions se fait de manière quasi-transparente et n'est visible que dans la fonction utilisant l'union. Les objets stockés dans l'union ne sont pas impacté et l'utilisateur de la fonction appelle la fonction de manière naturelle avec en paramètre un pointeur sur l'objet, l'appel implicite du constructeur de l'union étant invoqué par le compilateur.

Les classes Union sont disponible ici.