Pages

dimanche 30 novembre 2014

Appel conditionnel de fonction selon la validité d'une expression.

Il existe une méthode à base de trait pour changer le comportement d'une fonction. Je l'ai déjà présenté dans un article plus ancien en prenant comme exemple les itérateurs et la fonction std::advance: "taguer vos classes, cataloguées-les".

L'approche suivante consiste non plus à définir des types internes ou des traits mais vérifier qu'une fonction (membre ou statique) est appelable. De manière plus générale, la méthode présentée ici s'applique à toutes expressions.

Appeler T::sort si possible, sinon std::sort(begin(T), end(T))

L'exemple va se faire sur la classe std::list qui n'est pas triable avec std::sort mais possède une fonction membre sort(). Ainsi que sur std::vector qui, inversement, n'a pas de fonction membre sort() mais est triable avec std::sort.

La méthode est simple et consiste à créer 2 fonctions: une pour vérifie si une expression est valide (ici x.sort()) et une autre en cas d'échec.

Seulement, qui dit 2 fonctions dit 2 prototypes. Leur prototype doit être légèrement différent mais compatible avec les mêmes valeurs d'entrée pour appeler la seconde si la première échoue.

Pour vérifier l'expression, seul 2 mots clef existent: sizeof et decltype. Cette procédure est donc possible avant C++11, même si sizeof requière un peu d'enrobage.

#include <algorithm>

//avec decltype
template<class Container>
auto dispatch_sort(int, Container & c)
-> decltype(void(c.sort())) //force decltype au type void
{ c.sort(); }

template<class Container>
void dispatch_sort(unsigned, Container & c)
{
 using std::begin;
 using std::end;
 std::sort(begin(c), end(c));
}

template<class Cont>
void sort(Cont & c)
{ dispatch_sort(1,c); }

La fonction sort appel dispatch_sort avec un int (la valeur n'importe pas, seul le type compte). Comme la seule différence des 2 fonctions dispatch_sort est le premier paramètre, le prototype avec un int correspond parfaitement.

Si une fonction membre sort existe alors l'expression dans decltype est valide et la fonction est appelé. Dans le cas contraire, le compilateur cherche une fonction avec des paramètres pouvant être convertit. Le int pouvant être convertit en unsigned, le compilateur se rabat sur le second prototype qui fait appel à std::sort.

Le point clef étant de mettre toutes les informations dans le prototype. J'aurais par exemple put mettre decltype dans un paramètre initialisé avec une valeur par défaut (R f(int, decltype(xxx)* = 0); Mais il faudra probablement ajouter std::remove_reference (un pointeur sur une référence n'est pas permis))).


Programme de test:

#include <iostream>
#include <vector>
#include <list>

int main()
{
  std::vector<int> v({2,6,4});
  std::list<int>   l({2,6,4});

  sort(v);
  sort(l);

  for ( auto i : v ) std::cout << i << ' ';
  std::cout << '\n';
  for ( auto i : l ) std::cout << i << ' ';
}

Résultats:

2 4 6
2 4 6

J'ai indiqué qu'il été possible d'utiliser sizeof à la place de decltype. Voici comment:

template<std::size_t, class T = void>
struct dispatch_result_type
{ typedef T type; };

template<class T>
T declval();

template<class Container>
typename dispatch_result_type<sizeof(void(declval<Container&>().sort()),1)>::type
dispatch_sort(int, Container & c)
{ c.sort(); }

Le ,1 de sizeof(xxx,1) peut dérouter mais est requis si l'expression xxx retourne void. Comme void n'est pas vraiment un type, il ne fonctionne pas avec sizeof et il faut donc lui fournir autre chose. Il faut bien comprendre qu'ici xxx,1 est une seule expression et non pas 2 paramètres.

Bien que très peu probable, si j'ai mit void(yyy), c'est pour prévenir la surcharge de l'opérator ',' sur le type de retour retourné par yyy (car cet opérateur peut lui-même retourner un void).

sizeof ne donne pas l'information sur le type de retour mais une valeur, il est couplé à dispatch_result_type qui prend en second paramètre template le type de retour (void par défaut). Quand à declval, c'est le même principe que celui de la sl.