Vous comprendez mieux ce qui suit si vous avez une compréhension au moins superficielle des enjeux de programmation générique appliquée, et si vous savez ce que sont les traits. Puisque nous utiliserons tout particulièrement enable_if dans cet exemple (en attendant l'avènement des concepts de C++ 20), je vous invite à lire au préalable sur le sujet.
Il arrive que nous nous mettions les pieds dans les plats avec les meilleures intentions du monde. Pensons par exemple à une classe Tableau<T> (esquissée ici) dont les services d'approcheraient de ceux d'un std::vector<T>. |
|
Cette classe exposerait entre autres un constructeur acceptant en paramètre un nombre d'éléments et une valeur initiale, de manière à ce que ceci :
... affiche en pratique :
|
|
Cette classe exposerait aussi un constructeur acceptant en paramètre une séquence à demi-ouverte définie par une paire d'itérateurs, de manière à ce que ceci :
... affiche en pratique :
|
|
Le problème est que, si nous avons une définition comme celle-ci :
Tableau<int> tab{ 10, -1 }; // oups!
... le choix entre ces deux constructeurs devient ambigu : doit-on prendre la version acceptant un nombre d'éléments et une valeur (correct ici pour un Tableau<T> dont T est entier), ou doit-on plutôt prendre la version acceptant deux itérateurs de type It (correct ici car les deux paramètres sont du même type)?
Pourtant, il est possible de clarifier l'intention : le constructeur prenant une paire d'itérateurs ne devrait être considéré que si It est... un itérateur, or il n'y a pas vraiment de type itérateur en C++. En effet, certains primitifs (les pointeurs!) sont des itérateurs, et il n'y a pas de classe parent commune à tous les types qui se conforment au concept d'itérateur (sans les concepts de C++ 20, une simple surcharge ne suffira pas à départager entre ces deux constructeurs dans le cas soumis à votre attention).
Pour résoudre cet irritant, nous pouvons nous définir un trait is_iterator<T> qui sera vrai si T est un itérateur et faux sinon, puis utiliser enable_if pour éliminer le constructeur prenant une paire It,It en paramètre dans le cas où It ne serait pas un itérateur.
Il nous faut tout d'abord déterminer un critère permettant de savoir si un type T donné se qualifie en tant qu'itérateur. Pour cette démonstration, j'ai choisi de dire que T est un itérateur si le trait std::iterator_traits<T> existe; ce trait guide la générations des algorithmes standards qui utilisent des itérateurs, ce qui en fait un bon candidat.
Pour réaliser ce test d'existence, j'utiliserai trois leviers :
La fonction declval<T>(), qui n'existe pas mais permet de raisonner à la compilation sur un hypothétique T |
La fonction declval<T>() va comme suit :
Cette fonction n'a pas de définition, et c'est délibéré. Elle permet de poser des questions sur un type T sans avoir à l'instancier. Par exemple :
|
L'opérateur decltype(expr), qui permet de déduire le type de l'expression expr |
Par exemple :
|
Le trait std::void_t, qui permet de tester la validité d'une expression (grâce à SFINAE) |
Par exemple, has_type_member<T>::value sera vrai seulement si typename T::type est une expression valide :
|
Allons-y :
Le code suit (il y avait une erreur de retranscription dans la version originale de cet article; merci à @Onduril de me l'avoir indiqué!) :
#include <type_traits>
template <class, class = void>
struct is_iterator : std::false_type {};
template <class T>
struct is_iterator<T, std::void_t<decltype(std::declval<typename std::iterator_traits<T>::value_type>())>>
: std::true_type {
};
template <class T>
static constexpr bool is_iterator_v = is_iterator<T>();
Puisque nous désirons éliminer le constructeur de séquence dans le cas où le type It utilisé à titre de paramètres n'est pas conforme au concept des itérateurs, et puisque les constructeurs n'ont pas de type de retour, nous utiliserons un paramètre additionnel (bidon et anonyme) pour rendre la signature de ce constructeur invalide quand It n'est pas un itérateur :
template <class T>
class Tableau {
// ...
using value_type = T;
using size_type = std::size_t;
// ...
public:
// ...
// initialiser *this avec n copies de val
Tableau(size_type n, const value_type &val);
// ...
// initialiser *this avec une copie des éléments dans la séquence [debut,fin)
template <class It>
Tableau(It debut, It fin, std::enable_if_t<is_iterator_v<It>, void*> = {});
// ...
};
Pas mal, non?