C++: Déterminer le type de retour d'une opération arithmétique
Par soni le mardi 27 septembre 2005, 14:51 - Programmation - Editer
Soit deux types génériques T1 et T2. Comment récupérer le type de retour d'une opération arithmétique sur des instances de T1 et T2 ? Comment un problème simple a priori (écrire une fonction générique de produit scalaire) peut devenir un véritable casse-tête...
Voici le problème initial: écrire une fonction de produit scalaire générique, les deux vecteurs pouvant être de types différents (mais implémentant le même concept). La déclaration d'une telle fonction sera typiquement
template<typename TVector1, typename TVector2> value_type dot_product(const TVector1 &, const TVector2 &);
Le problème qui se pose est la détermination du type de retour value_type... Il s'agit ici du type correspondant au produit des éléments des vecteurs: si TVector1 est un vecteur de char et TVector2 un vecteur de double alors value_type doit être double.
La solution idéale est d'écrire un traits qui renvoie le type de retour du produit, par exemple product<T1,T2>::value_type. Nous proposons ici trois solutions plus ou moins partielles pour écrire product...
Solution 1: instanciations partielles
Une première solution (préconisé par Nawouak) consiste à profiter de l'instanciation partielle pour écrire product. Ainsi, pour une opération arithmétique donnée, il faut écrire les différentes instanciations partielles de la structures... L'ordre d'écriture des instanciations partielles est important et permet d'éviter d'écrire toute les combinaisons possibles. Voici un exemple avec quelques types:
template<typename T1, typename T2> struct product;
#define REGISTER_PRODUCT_TYPE_1(x,y,z)\
template <> struct product<x, y> { typedef z value_type; }; \
#define REGISTER_PRODUCT_TYPE_2(x)\
template<typename T> struct product<x, T> { typedef x value_type; }; \
template<typename T> struct product<T, x> { typedef x value_type; };
REGISTER_PRODUCT_TYPE_1(Matrix,Vector,Vector)
REGISTER_PRODUCT_TYPE_1(Vector,double,Vector)
REGISTER_PRODUCT_TYPE_1(double,Vector,Vector)
REGISTER_PRODUCT_TYPE_2(double)
REGISTER_PRODUCT_TYPE_2(int)
REGISTER_PRODUCT_TYPE_2(unsigned int)
...
Avantages: solution statique, portable (le compilateur doit supporter l'instanciation partielle).
Inconvénients: fastidieux, peut devenir complexe (le produit d'un vecteur par un scalaire par exemple vector<int> * double => vector<double> ? mais cela n'entre pas vraiment dans le cadre du simple produit scalaire...).
Solution 2: le typeof du gcc
C'est, de loin la solution la plus satisfaisante et la plus simple mais elle utilise une extension spécifique de gcc...
typeof est une instruction spécifique au gcc qui renvoie le type d'une expression, c'est donc exactement ce dont nous avons besoin. Voici l'écriture de notre traits:
template<typename T1, typename T2>
struct product
{
typedef typeof(T1() * T2()) value_type;
};
Cela marche nickel (il faut tout de même que les types T1 et T2 aient des constructeurs par défaut). Pour que le code reste valable même avec l'option -ansi de gcc, il faut utiliser __typeof__.
Avantages: solution simple, statique, valable dès que le produit est possible entre deux types.
Inconvénients: non ansi, spécifique au gcc.
Solution 3: écriture d'un typeof portable
L'idée de cette solution est d'écrire un typeof portable. C'est une solution proposée par Bill Gibbons. Cette astuce est par ailleurs utilisée dans la boost...
L'idée de Bill Gibbons est d'utiliser l'instruction sizeof et d'associer un type de donnée de taille spécifique à chaque type de retour possible. Cette taille est ensuite utilisée comme paramètre de template permettant d'accéder au type recherché. Un bout de code est toujours plus explicite (la classe WrapType permet de prendre en compte des types complexes comme les pointeurs de fonctions) :
template<int N> struct typeof_class;
template<class T> struct WrapType { typedef T U; };
#define REGISTER_TYPEOF(N,T) \
template<> struct typeof_class<N> { typedef WrapType<T>::U V; }; \
char (*typeof_fct(const WrapType<T>::U &))[N];
#define my_typeof(x) typeof_class<sizeof(*typeof_fct(x))>::V
REGISTER_TYPEOF(1,bool)
REGISTER_TYPEOF(2,char)
REGISTER_TYPEOF(3,unsigned int)
REGISTER_TYPEOF(4,int)
REGISTER_TYPEOF(5,float)
REGISTER_TYPEOF(6,double)
REGISTER_TYPEOF(7,Vector)
template<typename T1, typename T2>
struct product
{
typedef typename my_typeof(T1() * T2()) value_type;
};
Ca marche correctement. Le compilateur doit tout de même supporter les instanciations partielles. Il ne faut pas oublier typename dans la structure product...
Avantages: solution statique, valable dès que le type de retour est déclaré.
Inconvénients: il faut déclarer les types de retour possibles (l'ordre n'est pas important).
Conclusion
La dernière solution permet d'écrire notre fonction dot_product avec le bon type de retour.
N'hésitez pas à proposer vos solutions !
Commentaires
Trop trop fort !!! ... même si je n'ai pas tout compris
Très bonne astuce d'utiliser
sizeofpour l'évaluation statique d'une expression (même si la manip. avec les tableaux n'est pas très belle !). Malheureusement, j'ai testé avec C++ Builder et Visual C++, et il y a des problèmes.En effet, impossible de faire
sizeof(T1())dans le typedef, par contre on peut fairesizeof(product<T1,T2>::a)où a est un attribut de classe de type T1. Ca passe sur les deux compilateurs.Ensuite, j'ai essayé de faire
sizeof(product<T1,T2>::a*product<T1,T2>::b). Avec C++ Builder, ça compile, mais ça donne une taille de 4 au lieu de 8, donc ça marche pas ! Avec Visual C++, ça donne une erreur interne du compilateur.De toute façon, mettre une fonction (comme typeof_fct) dans le
sizeofest rejeté par les deux compilateursMais tout espoir n'est pas perdu. En effet, sur une page de The Code Project, il est expliqué que la Boost utilise la technique de Gibbons, et ça compile avec Visual ! Le problème est de comprendre ce qui se passe dans la Boost, il n'y a pas de documentation sur le sujet. A étudier donc...