L'idiome de détection de C++ – le type void_t

À 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

Quelques exemples d'utilisation suivent.

Détecter une fonction d'une signature donnée

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
}

Détection d'un type interne et public

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);
}

Détecter un constructeur

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);
}

Détecter un opérateur

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.

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !