À CppCon 2014, l'éminent (et fort sympathique) Walter E. Brown a donné une conférence en deux parties sur la métaprogrammation, dans laquelle il a présenté son idée du type std::void_t, ce qui lui a valu une ovation de la salle. Cette idée, qu'il a aussi surnommé « The C++ Detection Idiom », permet avec aisance de définir un trait détectant le support (ou pas) d'une fonctionnalité, par exemple l'existence d'un opérateur + entre deux T ou la présence d'une méthode T::m(U).
Le type std::void_t prend la forme suivante :
template <class...> using void_t = void;
L'idée derrière std::void_t est simple : utiliser une expression qui peut être assujetti à SFINAE pour exclure les expressions malformées, et ainsi détecter celles pour lesquelles un programme est porteur de sens.
Quelques exemples d'utilisation suivent.
Supposons par exemple que nous souhaitons détecter l'existence (ou non) d'une fonction f(T,T,float) pour certains types T. Cette tâche est toute indiquée pour std::void_t :
#include <type_traits>
char f(char, char, float) = delete; // cas supprimé
template <class T>
T f(T, T, float) { // en général, f(T,T,float) existe et retourne un T par défaut
return {};
}
float f(const float&, const float&, float) { // pour des const float&, elle retourne une valeur fixe
return 1.5f;
}
template <class, class = std::void_t<>>
struct f_existe : std::false_type {
};
template <class T>
struct f_existe<T, std::void_t<decltype( f(std::declval<T>(),std::declval<T>(),float{}) )>> : std::true_type {
};
int main() {
static_assert(!f_existe<char>::value); // le cas supprime
static_assert(f_existe<float>::value); // la fonction qui prend des float
static_assert(f_existe<int>::value); // le template general
}
Supposons maintenant que nous souhaitions détecter la présence d'un type interne et public nommé type dans un type T donné :
#include <type_traits>
struct X { // pas de type interne et public type
};
struct Y {
using type = int; // un type interne et public type
};
template <class, class = std::void_t<>>
struct possede_type : std::false_type {
};
template <class T>
struct possede_type<T, std::void_t<typename T::type>> : std::true_type {
};
int main() {
static_assert(!possede_type<X>::value);
static_assert(possede_type<Y>::value);
}
Supposons maintenant que l'on souhaite savoir si un type donné est constructible à partir d'un int :
#include <type_traits>
struct X {
};
struct Y {
Y(int) {}
};
template <class, class = std::void_t<>>
struct constructible_par_int : std::false_type {
};
template <class T>
struct constructible_par_int<T, std::void_t<decltype(T(std::declval<int>()))>> : std::true_type {
};
int main() {
static_assert(!constructible_par_int<X>::value);
static_assert(constructible_par_int<Y>::value);
}
Supposons enfin que l'on souhaite savoir si un type donné est multipliable, au sens où multiplier un T par un T à l'aide de l'opérateur * est légal :
#include <type_traits> struct X { }; struct Y { };
Y operator*(Y,Y); template <class, class = std::void_t<>> struct multipliable : std::false_type { }; template <class T> struct multipliable<T, std::void_t<decltype(std::declval<T>() * std::declval<T>())>> : std::true_type { }; int main() { static_assert(!multipliable<X>::value); static_assert(multipliable<Y>::value);
}
Manifestement, à l'aide de std::void_t, un programme peut détecter une fonctionnalité à la compilation de manière simple et élégante, le tout sur la base d'une expression.
Quelques liens pour enrichir le propos.