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.