Préprocesseur et énumérations : la suite.


Cet article fait suite à mon premier article intitulé "Faire un peu joujou avec le préprocesseur"
(disponible ici : http://www.coder-studio.com/blog/preproc/ ).

Pour rappel, nous en sommes restés à un système de macro qui permettait d'automatiser la création
d'un opérateur "<<" pour iostream, capable d'afficher le nom d'une énumération.

Voici le code :

// Enumerate.h
 
#ifndef ENUMERATE_H
#define ENUMERATE_H
 
#define MAKE_ENUM(enum_name, ...)	\
	enum enum_name { __VA_ARGS__ };
 
#define ENUM_CASE_1(val)	\
	case val:	\
		os << #val;	\
		break;
 
#define ENUM_CASE_2(val, ...)	ENUM_CASE_1(val) ENUM_CASE_1(__VA_ARGS__)
#define ENUM_CASE_3(val, ...)	ENUM_CASE_1(val) ENUM_CASE_2(__VA_ARGS__)
#define ENUM_CASE_4(val, ...)	ENUM_CASE_1(val) ENUM_CASE_3(__VA_ARGS__)
#define ENUM_CASE_5(val, ...)	ENUM_CASE_1(val) ENUM_CASE_4(__VA_ARGS__)
#define ENUM_CASE_6(val, ...)	ENUM_CASE_1(val) ENUM_CASE_5(__VA_ARGS__)
#define ENUM_CASE_7(val, ...)	ENUM_CASE_1(val) ENUM_CASE_6(__VA_ARGS__)
#define ENUM_CASE_8(val, ...)	ENUM_CASE_1(val) ENUM_CASE_7(__VA_ARGS__)
#define ENUM_CASE_9(val, ...)	ENUM_CASE_1(val) ENUM_CASE_8(__VA_ARGS__)
#define ENUM_CASE_10(val, ...)	ENUM_CASE_1(val) ENUM_CASE_9(__VA_ARGS__)
 
#define ENUM_CASE_N(nb_vals, ...) ENUM_CASE_ ## nb_vals(__VA_ARGS__)
 
#define MAKE_OPERATOR(nb_vals, enum_name, ...)	\
	inline std::ostream& operator<<(std::ostream& os, const enum_name& e)	{\
		switch(e) {	\
			ENUM_CASE_N(nb_vals, __VA_ARGS__)	\
		}	\
		return os;	\
	}
 
 
#define ENUMERATE(nb_vals, enum_name, ...)	\
	MAKE_ENUM(enum_name, __VA_ARGS__)	\
	MAKE_OPERATOR(nb_vals, enum_name, __VA_ARGS__)
 
#endif // ENUMERATE_H
// main.cpp
 
#include <iostream>
#include "Enumerate.h"
 
ENUMERATE(3, MonEnumeration,
	E_VAL_1,
	E_VAL_2,
	E_VAL_3);
 
int main()
{
	MonEnumeration e = E_VAL_1;
	std::cout << e << std::endl;
	return 0;
}

Le plus gros problème de ce système est le fait que l'on soit obligés de donner le nombre de valeurs de l'énumération
lors de l'utilisation de la macro ENUMERATE.

Ce qui nous oblige à spécifier ce numéro, c'est la nécessiter de "choisir" la bonne macro parmi ENUM_CASE_1, ENUM_CASE_2, ..., ENUM_CASE_10.

Se passer de ce nombre, c'est effectuer l' "appel récursif" jusqu'à ce qu'il n'y ait plus d'argument à traiter dans la
"liste" __VA_ARGS__.

Que se passe-t-il si l'on supprime ce nombre et que l'on remplace l'appel à ENUM_CASE_N par la version qui gère le plus d'arguments
(ici ENUM_CASE_10, donc) ?

Version remaniée :

// Enumerate.h
 
#ifndef ENUMERATE_H
#define ENUMERATE_H
 
#define MAKE_ENUM(enum_name, ...)	\
	enum enum_name { __VA_ARGS__ };
 
#define ENUM_CASE_1(val)	\
	case val:	\
		os << #val;	\
		break;
 
#define ENUM_CASE_2(val, ...)	ENUM_CASE_1(val) ENUM_CASE_1(__VA_ARGS__)
#define ENUM_CASE_3(val, ...)	ENUM_CASE_1(val) ENUM_CASE_2(__VA_ARGS__)
#define ENUM_CASE_4(val, ...)	ENUM_CASE_1(val) ENUM_CASE_3(__VA_ARGS__)
#define ENUM_CASE_5(val, ...)	ENUM_CASE_1(val) ENUM_CASE_4(__VA_ARGS__)
#define ENUM_CASE_6(val, ...)	ENUM_CASE_1(val) ENUM_CASE_5(__VA_ARGS__)
#define ENUM_CASE_7(val, ...)	ENUM_CASE_1(val) ENUM_CASE_6(__VA_ARGS__)
#define ENUM_CASE_8(val, ...)	ENUM_CASE_1(val) ENUM_CASE_7(__VA_ARGS__)
#define ENUM_CASE_9(val, ...)	ENUM_CASE_1(val) ENUM_CASE_8(__VA_ARGS__)
#define ENUM_CASE_10(val, ...)	ENUM_CASE_1(val) ENUM_CASE_9(__VA_ARGS__)
 
#define MAKE_OPERATOR(enum_name, ...)	\
	inline std::ostream& operator<<(std::ostream& os, const enum_name& e)	{\
		switch(e) {	\
			ENUM_CASE_10(__VA_ARGS__)	\
		}	\
		return os;	\
	}
 
 
#define ENUMERATE(enum_name, ...)	\
	MAKE_ENUM(enum_name, __VA_ARGS__)	\
	MAKE_OPERATOR(enum_name, __VA_ARGS__)
 
#endif // ENUMERATE_H
// test.cpp
 
#include "Enumerate.h"
 
ENUMERATE(MonEnumeration,
	E_VAL_1,
	E_VAL_2,
	E_VAL_3);

Après un appel au préprocesseur par "cpp test.cpp", voici ce qu'il nous affiche (remis en forme pour être lisible et
en enlevant les lignes commençant par un #, sans importance) :

enum MonEnumeration { E_VAL_1, E_VAL_2, E_VAL_3 };
 
inline std::ostream& operator<<(std::ostream& os, const MonEnumeration& e)
{
	switch(e)
	{
	case E_VAL_1: os << "E_VAL_1"; break;
	case E_VAL_2: os << "E_VAL_2"; break;
	case E_VAL_3: os << "E_VAL_3"; break;
	case : os << ""; break;
	case : os << ""; break;
	case : os << ""; break;
	case : os << ""; break;
	case : os << ""; break;
	case : os << ""; break;
	case : os << ""; break;
	}
	return os;
};

Vous voyez le problème : on a des "case :" à partir du stade où il n'y a plus d'argument. Il faut donc remplacer
chaque ligne "case ..." (correspondant à ce que génère ENUM_CASE_1) par quelque chose qui puisse faire un test d'égalité
entre "e" et "E_VAL_1", "E_VAL_2"...etc, mais qui puisse aussi compiler et ne rien faire lorsqu'en lieu et place
de "E_VAL_XXX", il n'y a rien.

Là, on pense à la surcharge de fonction ! En effet, si l'on a une fonction surchargée ainsi :

void f(int i)
{
	// blabla
}
 
void f()
{
	// blublu
}

on peut obtenir un comportement différent selon que l'on passe ou pas quelque chose en argument à la fonction !
Il suffirait de remplacer chaque ligne "case ..." par un test d'égalité qui aurait cette forme :

if(TestEgalite(e, E_VAL_1)) {os << "E_VAL_1"; return os;}

et le tour est joué !

Mais attendez, vous ne voyez pas un problème ? Si, si : contrairement à la fonction f() de l'exemple, ce "TestEgalite"
a 2 arguments. Si l'on remplace "E_VAL_1" par du vide, on obtient un "if(TestEgalite(e, )) {blabla;}" : ça ne marche pas !

Le problème reste donc entier...
Oui, pour ceux qui se demandent, cette partie de l'article n'apporte rien à la solution, c'est juste la démarche que j'ai
eue : vous pouvez vous dire que ça ne sert à rien, je ne vous le reprocherai pas, mais maintenant c'est trop tard, vous
l'avez lu :p

On pourrait se poser le problème ainsi : comment, à partir d'une fonction à 2 arguments, en faire 2 fonctions à 1 argument ?
En effet, si l'on arrive à en faire 2 fonctions à 1 argument, alors le problème serait réglé :) Si je ne m'abuse, il
s'agit de la "curryfication", que l'on trouve en programmation fonctionnelle et en lambda-calcul :)

Peut-être est-ce possible, je n'ai pas exploré cette possibilité. Il ne m'étonnerait pas que cela soit possible grâce à des
foncteurs, i.e. une classe dont un surcharge l'opérateur "()". On peut ainsi se retrouver avec une construction de la forme
"MaClasse(arg1)(arg2)", i.e. un appel au constructeur suivi d'un appel à l'opérateur "()".

En fait, à mon avis, c'est une solution possible, je garderai peut-être pour la suite, dans un hypothétique futur article :)

Quoi qu'il en soit, pour en revenir au problème : on peut aussi se le poser ainsi : est-on obligés de traiter les cas où
le préprocesseur ne met rien, en lieu et place de nos "E_VAL_1", "E_VAL_2"...etc ? Dit encore autrement, ne peut-on pas mettre
quelque chose d'autre à la place ? Qu'est-ce qui nous arrangerait alors ?

Hmm, voyons voir, remplacer le vide par quelque chose que l'on pourrait tester lors de la compilation...
Il s'agit bien de méta-programmation. Or, en méta-programmation : les 2 outils principaux que le C++ met à notre disposition
sont les macros et les templates.

Eurêka !

On peut "surcharger" une fonction template qui s'occupera de faire le test d'égalité !
On écrit alors les tests d'égalité comme ceci :

if(TestEgalite<E_VAL_1>(e)) {blabla;}
if(TestEgalite<E_VAL_2>(e)) {blabla;}
...
if(TestEgalite<void>(e)) {blabla;}

On déclare alors TestEgalite sous cette forme :

template <MonEnumeration val>
inline bool TestEgalite(const EnumType& e)
{
	return e == val;
}
 
template <class T>
inline bool TestEgalite(const EnumType& e)
{
	return false;
}

Et le tour est joué !

Bon, maintenant, pour faire plus pro, en renommant ça en anglais et en préfixant par le nom de l'énumération
(histoire que l'on n'ait pas de conflit s'il y a 2 énumérations différentes, au cas où), et en remettant tout
ensemble, cela donne ceci :

Version finale :

// Enumerate.h
 
#ifndef ENUMERATE_H
#define ENUMERATE_H
 
#define MAKE_ENUM(EnumType, ...)	\
	enum EnumType { __VA_ARGS__ };
 
#define ENUM_CASE_1(EnumType, val, ...)	\
	if(EnumType ## _EnumIsEqual<val>(e)) {os << #val; break;}
 
#define ENUM_CASE_2(EnumType, val, ...)		ENUM_CASE_1(EnumType, val) ENUM_CASE_1(EnumType, __VA_ARGS__, void)
#define ENUM_CASE_3(EnumType, val, ...)		ENUM_CASE_1(EnumType, val) ENUM_CASE_2(EnumType, __VA_ARGS__, void)
#define ENUM_CASE_4(EnumType, val, ...)		ENUM_CASE_1(EnumType, val) ENUM_CASE_3(EnumType, __VA_ARGS__, void)
#define ENUM_CASE_5(EnumType, val, ...)		ENUM_CASE_1(EnumType, val) ENUM_CASE_4(EnumType, __VA_ARGS__, void)
#define ENUM_CASE_6(EnumType, val, ...)		ENUM_CASE_1(EnumType, val) ENUM_CASE_5(EnumType, __VA_ARGS__, void)
#define ENUM_CASE_7(EnumType, val, ...)		ENUM_CASE_1(EnumType, val) ENUM_CASE_6(EnumType, __VA_ARGS__, void)
#define ENUM_CASE_8(EnumType, val, ...)		ENUM_CASE_1(EnumType, val) ENUM_CASE_7(EnumType, __VA_ARGS__, void)
#define ENUM_CASE_9(EnumType, val, ...)		ENUM_CASE_1(EnumType, val) ENUM_CASE_8(EnumType, __VA_ARGS__, void)
#define ENUM_CASE_10(EnumType, val, ...)	ENUM_CASE_1(EnumType, val) ENUM_CASE_9(EnumType, __VA_ARGS__, void)
 
#define MAKE_OPERATOR(EnumType, ...)	\
	template <EnumType val>				\
	inline bool EnumType ## _EnumIsEqual(const EnumType& e) {	\
		return e == val;										\
	}															\
																\
	template <class T>											\
	inline bool EnumType ## _EnumIsEqual(const EnumType& e) {	\
		return false;											\
	}															\
																\
	inline std::ostream& operator<<(std::ostream& os, const EnumType& e) {	\
		do {																\
			ENUM_CASE_10(EnumType, __VA_ARGS__, void)								\
		} while(0);											\
		return os;											\
	}
 
#define ENUMERATE(EnumType, ...)		\
	MAKE_ENUM(EnumType, __VA_ARGS__)	\
	MAKE_OPERATOR(EnumType, __VA_ARGS__)
 
#endif // ENUMERATE_H
// main.cpp
 
#include <iostream>
#include "Enumerate.h"
 
ENUMERATE(MonEnumeration,
	E_VAL_1,
	E_VAL_2,
	E_VAL_3);
 
int main()
{
	MonEnumeration e = E_VAL_1;
	std::cout << e << std::endl;
	return 0;
}

Voilà pour cette partie :)

Ceci dit, un problème est résolu, mais il en reste d'autres, notamment :
- ça doit toujours aussi mal passer auprès des systèmes d'auto-complétion (enfin, j'ai pas testé sur Visual Assist
ou Eclipse à vrai dire...)
- on ne peut toujours pas attribuer de valeur numérique manuellement à un symbole de l'énumération

Il pourrait aussi être intéressant d'associer une autre chaîne de caractères, voire toute autre valeur, à un symbole de
l'énumération...
Tout comme il serait intéressant d'explorer une solution à base de pseudo-curryfication plutôt que de templates, voire, qui
sait, une solution entièrement à base de templates. A ce sujet, un article très intéressant est disponible ici pour les
curieux : http://www.codeguru.com/cpp/cpp/cpp_mfc/templates/article.php/c4137

Ce sera tout pour cette fois !

-------------
EDIT

Petite édition du post original : un pote m'a proposé une petite amélioration du code original (Jérémie, si tu me lis ^^), qui rend le code plus clair et évite de passer par les templates.

L'idée est dans l'opérateur "<<" de créer un objet wrapper autour de ma valeur d'énumération, muni de fonctions de test d'égalité surchargées afin d'accepter un ou zéro argument.
Grâce à cette surcharge, on peut ainsi éviter de traîner les "void" utilisés en paramètres des templates, ce qui est quand même plus propre, et qui encore une fois si le compilateur optimise bien, ne devrait pas créer de différence :)

Trève de discours, voici la version finale du code :

// Enumerate.h
 
#ifndef ENUMERATE_H
#define ENUMERATE_H
 
#define MAKE_ENUM(enum_name, ...)	\
	enum enum_name { __VA_ARGS__ };
 
#define ENUM_CASE_1(val)	\
	if(s.isEqual(val))		\
		os << #val;
 
#define ENUM_CASE_2(val, ...)	ENUM_CASE_1(val) ENUM_CASE_1(__VA_ARGS__)
#define ENUM_CASE_3(val, ...)	ENUM_CASE_1(val) ENUM_CASE_2(__VA_ARGS__)
#define ENUM_CASE_4(val, ...)	ENUM_CASE_1(val) ENUM_CASE_3(__VA_ARGS__)
#define ENUM_CASE_5(val, ...)	ENUM_CASE_1(val) ENUM_CASE_4(__VA_ARGS__)
#define ENUM_CASE_6(val, ...)	ENUM_CASE_1(val) ENUM_CASE_5(__VA_ARGS__)
#define ENUM_CASE_7(val, ...)	ENUM_CASE_1(val) ENUM_CASE_6(__VA_ARGS__)
#define ENUM_CASE_8(val, ...)	ENUM_CASE_1(val) ENUM_CASE_7(__VA_ARGS__)
#define ENUM_CASE_9(val, ...)	ENUM_CASE_1(val) ENUM_CASE_8(__VA_ARGS__)
#define ENUM_CASE_10(val, ...)	ENUM_CASE_1(val) ENUM_CASE_9(__VA_ARGS__)
 
#define MAKE_OPERATOR(enum_name, ...)	\
	inline std::ostream& operator<<(std::ostream& os, const enum_name& e)	{\
		struct {							\
			enum_name e;					\
			inline bool isEqual(enum_name val) const {return e == val;}	\
			inline bool isEqual() const {return false;}					\
		} s;														\
		s.e = e;			\
		ENUM_CASE_10(__VA_ARGS__)	\
		return os;	\
	}
 
 
#define ENUMERATE(enum_name, ...)	\
	MAKE_ENUM(enum_name, __VA_ARGS__)	\
	MAKE_OPERATOR(enum_name, __VA_ARGS__)
 
#endif // ENUMERATE_H

Et voilà !

, , ,

  1. #1 by Twxs - juillet 15th, 2009 at 21:59

    le seul truc qui me gène c'est la limite du nombre d'enum, par exemple l'enum que je t'ai montré ce midi a plus de 200 elements, mis a part ce point c'est très beau!

  2. #2 by Funto - juillet 16th, 2009 at 23:22

    Merci :)

    + de 200, j'avais pas vu...là effectivement, ça fait bcp O_o

    Mais si on veut éviter d'être limité par le nombre de valeurs, on ne peut plus avoir recours à la pseudo-récursivité de macros alors, et là je vois pas vraiment comment faire...sinon faire comme sur le lien que j'ai indiqué, avec des templates, mais ça ne définit pas une "vraie" énumération.

    Ceci dit, contrairement à ce que tu m'as montré, là c'est géré en statique...

    PS : ton avatar est énorme, Rody powa :)

  3. #3 by Calvin1602 - juillet 26th, 2009 at 23:05

    Toujours pas motivé par le tableau indexé par l'enum ? ^^
    Sinon ya pas vraiment de limitation à 10, suffit d'augmenter la taille du code source :D Boost est limité à 256, je crois. Ya un fichier où ya que ça : les 256 versions de la même macro ^^

  4. #4 by Calvin1602 - juillet 26th, 2009 at 23:31

    // Enumerate.h

    #ifndef ENUMERATE_H
    #define ENUMERATE_H

    #define MAKE_ENUM(enum_name, ...) \
    enum enum_name { __VA_ARGS__ };

    #define ENUM_CASE_1(val) #val ,

    #define ENUM_CASE_2(val, ...) ENUM_CASE_1(val) ENUM_CASE_1(__VA_ARGS__)
    #define ENUM_CASE_3(val, ...) ENUM_CASE_1(val) ENUM_CASE_2(__VA_ARGS__)
    #define ENUM_CASE_4(val, ...) ENUM_CASE_1(val) ENUM_CASE_3(__VA_ARGS__)
    #define ENUM_CASE_5(val, ...) ENUM_CASE_1(val) ENUM_CASE_4(__VA_ARGS__)
    #define ENUM_CASE_6(val, ...) ENUM_CASE_1(val) ENUM_CASE_5(__VA_ARGS__)
    #define ENUM_CASE_7(val, ...) ENUM_CASE_1(val) ENUM_CASE_6(__VA_ARGS__)
    #define ENUM_CASE_8(val, ...) ENUM_CASE_1(val) ENUM_CASE_7(__VA_ARGS__)
    #define ENUM_CASE_9(val, ...) ENUM_CASE_1(val) ENUM_CASE_8(__VA_ARGS__)
    #define ENUM_CASE_10(val, ...) ENUM_CASE_1(val) ENUM_CASE_9(__VA_ARGS__)

    #define MAKE_OPERATOR(enum_name, ...) \
    inline std::ostream& operator<<(std::ostream& os, const enum_name& e) {\
    static const char * v[]={ \
    ENUM_CASE_10(__VA_ARGS__) \
    }; os << v[e]; \
    return os; \
    }

    #define ENUMERATE(enum_name, ...) \
    MAKE_ENUM(enum_name, __VA_ARGS__) \
    MAKE_OPERATOR(enum_name, __VA_ARGS__)

    #endif // ENUMERATE_H

    et

    ENUMERATE(MonEnumeration,
    E_VAL_1,
    E_VAL_2,
    E_VAL_3);

    int main()
    {
    MonEnumeration e = E_VAL_2;
    std::cout << e << std::endl;
    return 0;
    }

(will not be published)

  1. No trackbacks yet.