Je veux entrer dans plus de méta-programmation de modèles. Je sais que SFINAE signifie «l'échec de la substitution n'est pas une erreur». Mais quelqu'un peut-il me montrer une bonne utilisation de SFINAE?
Je veux entrer dans plus de méta-programmation de modèles. Je sais que SFINAE signifie «l'échec de la substitution n'est pas une erreur». Mais quelqu'un peut-il me montrer une bonne utilisation de SFINAE?
Réponses:
Voici un exemple (à partir d'ici ):
template<typename T>
class IsClassT {
private:
typedef char One;
typedef struct { char a[2]; } Two;
template<typename C> static One test(int C::*);
// Will be chosen if T is anything except a class.
template<typename C> static Two test(...);
public:
enum { Yes = sizeof(IsClassT<T>::test<T>(0)) == 1 };
enum { No = !Yes };
};
Quand IsClassT<int>::Yes
est évalué, 0 ne peut pas être converti en int int::*
car int n'est pas une classe, il ne peut donc pas avoir de pointeur de membre. Si SFINAE n'existait pas, alors vous obtiendrez une erreur de compilation, quelque chose comme «0 ne peut pas être converti en pointeur de membre pour le type non-classe int». Au lieu de cela, il utilise simplement le ...
formulaire qui renvoie Two, et évalue donc à false, int n'est pas un type de classe.
...
, mais plutôt le int C::*
, que je n'avais jamais vu et que je devais aller chercher. Vous avez trouvé la réponse à ce que c'est et à quoi cela pourrait être utilisé ici: stackoverflow.com/questions/670734/…
J'aime utiliser SFINAE
pour vérifier les conditions booléennes.
template<int I> void div(char(*)[I % 2 == 0] = 0) {
/* this is taken when I is even */
}
template<int I> void div(char(*)[I % 2 == 1] = 0) {
/* this is taken when I is odd */
}
Cela peut être très utile. Par exemple, je l'ai utilisé pour vérifier si une liste d'initialiseurs collectée à l'aide de la virgule d'opérateur n'est pas plus longue qu'une taille fixe
template<int N>
struct Vector {
template<int M>
Vector(MyInitList<M> const& i, char(*)[M <= N] = 0) { /* ... */ }
}
La liste n'est acceptée que lorsque M est plus petit que N, ce qui signifie que la liste d'initialisation n'a pas trop d'éléments.
La syntaxe char(*)[C]
signifie: Pointeur vers un tableau avec le type d'élément char et size C
. Si C
est faux (0 ici), alors nous obtenons le type invalide char(*)[0]
, pointeur vers un tableau de taille zéro: SFINAE fait en sorte que le modèle soit alors ignoré.
Exprimé avec boost::enable_if
, ça ressemble à ça
template<int N>
struct Vector {
template<int M>
Vector(MyInitList<M> const& i,
typename enable_if_c<(M <= N)>::type* = 0) { /* ... */ }
}
En pratique, je trouve souvent la capacité de vérifier les conditions une capacité utile.
M <= N ? 1 : -1
- être pourrait fonctionner à la place.
int foo[0]
. Je ne suis pas surpris qu'il soit supporté, car il permet l'astuce très utile "struct se terminant par un tableau de longueur 0" ( gcc.gnu.org/onlinedocs/gcc/Zero-Length.html ).
error C2466: cannot allocate an array of constant size 0
En C ++ 11, les tests SFINAE sont devenus beaucoup plus jolis. Voici quelques exemples d'utilisations courantes:
Choisissez une surcharge de fonction en fonction des traits
template<typename T>
std::enable_if_t<std::is_integral<T>::value> f(T t){
//integral version
}
template<typename T>
std::enable_if_t<std::is_floating_point<T>::value> f(T t){
//floating point version
}
En utilisant un idiome de type récepteur, vous pouvez faire des tests assez arbitraires sur un type comme vérifier s'il a un membre et si ce membre est d'un certain type
//this goes in some header so you can use it everywhere
template<typename T>
struct TypeSink{
using Type = void;
};
template<typename T>
using TypeSinkT = typename TypeSink<T>::Type;
//use case
template<typename T, typename=void>
struct HasBarOfTypeInt : std::false_type{};
template<typename T>
struct HasBarOfTypeInt<T, TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>> :
std::is_same<typename std::decay<decltype(std::declval<T&>().*(&T::bar))>::type,int>{};
struct S{
int bar;
};
struct K{
};
template<typename T, typename = TypeSinkT<decltype(&T::bar)>>
void print(T){
std::cout << "has bar" << std::endl;
}
void print(...){
std::cout << "no bar" << std::endl;
}
int main(){
print(S{});
print(K{});
std::cout << "bar is int: " << HasBarOfTypeInt<S>::value << std::endl;
}
Voici un exemple en direct: http://ideone.com/dHhyHE J'ai aussi récemment écrit une section entière sur SFINAE et l'envoi de balises dans mon blog (plug sans vergogne mais pertinent) http://metaporky.blogspot.de/2014/08/ part-7-static-dispatch-function.html
Notez que depuis C ++ 14, il y a un std :: void_t qui est essentiellement le même que mon TypeSink ici.
TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>
à un endroit puis TypeSinkT<decltype(&T::bar)>
à un autre? Est-ce aussi le &
nécessaire std::declval<T&>
?
TypeSink
, C ++ 17 ont std::void_t
:)
La bibliothèque enable_if de Boost offre une belle interface propre pour utiliser SFINAE. L'un de mes exemples d'utilisation préférés se trouve dans la bibliothèque Boost.Iterator . SFINAE est utilisé pour activer les conversions de type d'itérateur.
C ++ 17 fournira probablement un moyen générique de rechercher des fonctionnalités. Voir N4502 pour plus de détails, mais comme exemple autonome, considérez ce qui suit.
Cette partie est la partie constante, mettez-la dans un en-tête.
// See http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4502.pdf.
template <typename...>
using void_t = void;
// Primary template handles all types not supporting the operation.
template <typename, template <typename> class, typename = void_t<>>
struct detect : std::false_type {};
// Specialization recognizes/validates only types supporting the archetype.
template <typename T, template <typename> class Op>
struct detect<T, Op, void_t<Op<T>>> : std::true_type {};
L'exemple suivant, tiré de N4502 , montre l'utilisation:
// Archetypal expression for assignment operation.
template <typename T>
using assign_t = decltype(std::declval<T&>() = std::declval<T const &>())
// Trait corresponding to that archetype.
template <typename T>
using is_assignable = detect<T, assign_t>;
Par rapport aux autres implémentations, celle-ci est assez simple: un ensemble réduit d'outils ( void_t
et detect
) suffit. En outre, il a été signalé (voir N4502 ) qu'il est nettement plus efficace (temps de compilation et consommation de mémoire du compilateur) que les approches précédentes.
Voici un exemple en direct , qui inclut des modifications de portabilité pour GCC pre 5.1.
Voici un autre exemple (tardif) de SFINAE , basé sur la réponse de Greg Rogers :
template<typename T>
class IsClassT {
template<typename C> static bool test(int C::*) {return true;}
template<typename C> static bool test(...) {return false;}
public:
static bool value;
};
template<typename T>
bool IsClassT<T>::value=IsClassT<T>::test<T>(0);
De cette façon, vous pouvez vérifier la value
valeur de s pour voir si T
c'est une classe ou non:
int main(void) {
std::cout << IsClassT<std::string>::value << std::endl; // true
std::cout << IsClassT<int>::value << std::endl; // false
return 0;
}
int C::*
dans votre réponse? Comment peut C::*
être un nom de paramètre?
int C::*
est le type d'un pointeur vers une int
variable membre de C
.
Voici un bon article de SFINAE: Une introduction au concept SFINAE de C ++: introspection à la compilation d'un membre de classe .
Résumez-le comme suit:
/*
The compiler will try this overload since it's less generic than the variadic.
T will be replace by int which gives us void f(const int& t, int::iterator* b = nullptr);
int doesn't have an iterator sub-type, but the compiler doesn't throw a bunch of errors.
It simply tries the next overload.
*/
template <typename T> void f(const T& t, typename T::iterator* it = nullptr) { }
// The sink-hole.
void f(...) { }
f(1); // Calls void f(...) { }
template<bool B, class T = void> // Default template version.
struct enable_if {}; // This struct doesn't define "type" and the substitution will fail if you try to access it.
template<class T> // A specialisation used if the expression is true.
struct enable_if<true, T> { typedef T type; }; // This struct do have a "type" and won't fail on access.
template <class T> typename enable_if<hasSerialize<T>::value, std::string>::type serialize(const T& obj)
{
return obj.serialize();
}
template <class T> typename enable_if<!hasSerialize<T>::value, std::string>::type serialize(const T& obj)
{
return to_string(obj);
}
declval
est un utilitaire qui vous donne une "fausse référence" à un objet d'un type qui ne peut pas être facilement construit. declval
est vraiment pratique pour nos constructions SFINAE.
struct Default {
int foo() const {return 1;}
};
struct NonDefault {
NonDefault(const NonDefault&) {}
int foo() const {return 1;}
};
int main()
{
decltype(Default().foo()) n1 = 1; // int n1
// decltype(NonDefault().foo()) n2 = n1; // error: no default constructor
decltype(std::declval<NonDefault>().foo()) n2 = n1; // int n2
std::cout << "n2 = " << n2 << '\n';
}
Ici, j'utilise la surcharge de fonction de modèle (pas directement SFINAE) pour déterminer si un pointeur est une fonction ou un pointeur de classe membre: ( Est-il possible de corriger les pointeurs de fonction membre iostream cout / cerr imprimés comme 1 ou true? )
#include<iostream>
template<typename Return, typename... Args>
constexpr bool is_function_pointer(Return(*pointer)(Args...)) {
return true;
}
template<typename Return, typename ClassType, typename... Args>
constexpr bool is_function_pointer(Return(ClassType::*pointer)(Args...)) {
return true;
}
template<typename... Args>
constexpr bool is_function_pointer(Args...) {
return false;
}
struct test_debugger { void var() {} };
void fun_void_void(){};
void fun_void_double(double d){};
double fun_double_double(double d){return d;}
int main(void) {
int* var;
std::cout << std::boolalpha;
std::cout << "0. " << is_function_pointer(var) << std::endl;
std::cout << "1. " << is_function_pointer(fun_void_void) << std::endl;
std::cout << "2. " << is_function_pointer(fun_void_double) << std::endl;
std::cout << "3. " << is_function_pointer(fun_double_double) << std::endl;
std::cout << "4. " << is_function_pointer(&test_debugger::var) << std::endl;
return 0;
}
Tirages
0. false
1. true
2. true
3. true
4. true
Tel que le code est, il pourrait (selon la "bonne" volonté du compilateur) générer un appel à l'exécution à une fonction qui retournera vrai ou faux. Si vous souhaitez forcer l' is_function_pointer(var)
évaluation au type de compilation (aucun appel de fonction effectué au moment de l'exécution), vous pouvez utiliser l' constexpr
astuce variable:
constexpr bool ispointer = is_function_pointer(var);
std::cout << "ispointer " << ispointer << std::endl;
Par le standard C ++, toutes les constexpr
variables sont garanties d'être évaluées au moment de la compilation ( Calcul de la longueur d'une chaîne C au moment de la compilation. Est-ce vraiment une constexpr? ).
Le code suivant utilise SFINAE pour permettre au compilateur de sélectionner une surcharge en fonction du fait qu'un type a une certaine méthode ou non:
#include <iostream>
template<typename T>
void do_something(const T& value, decltype(value.get_int()) = 0) {
std::cout << "Int: " << value.get_int() << std::endl;
}
template<typename T>
void do_something(const T& value, decltype(value.get_float()) = 0) {
std::cout << "Float: " << value.get_float() << std::endl;
}
struct FloatItem {
float get_float() const {
return 1.0f;
}
};
struct IntItem {
int get_int() const {
return -1;
}
};
struct UniversalItem : public IntItem, public FloatItem {};
int main() {
do_something(FloatItem{});
do_something(IntItem{});
// the following fails because template substitution
// leads to ambiguity
// do_something(UniversalItem{});
return 0;
}
Production:
Flotteur: 1 Int: -1