Exemples C ++ SFINAE?


122

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?


2
C'est une bonne question. Je comprends assez bien SFINAE, mais je ne pense pas avoir jamais eu à l'utiliser (à moins que les bibliothèques ne le fassent sans que je le sache).
Zifre

5
STL le dit légèrement différemment dans la FAQ ici , "L'échec de la substitution n'est pas un éléphant"
vulcan raven

Réponses:


72

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>::Yesest é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.


8
@rlbond, j'ai répondu à votre question dans les commentaires de cette question ici: stackoverflow.com/questions/822059/… . En bref: si les deux fonctions de test sont candidates et viables, alors "..." a le plus mauvais coût de conversion, et ne sera donc jamais pris en faveur de l'autre fonction. "..." est l'ellipse, chose var-arg: int printf (char const *, ...);
Johannes Schaub - litb


20
La chose la plus étrange ici IMO n'est pas le ..., 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/…
HostileFork dit ne pas faire confiance SE

1
quelqu'un peut-il expliquer ce qu'est C :: *? J'ai lu tous les commentaires et liens, mais je me demande toujours, int C :: * signifie qu'il s'agit d'un pointeur de membre de type int. et si une classe n'a pas de membre de type int? Qu'est-ce que je rate? et comment le test <T> (0) joue-t-il là-dedans? Je dois manquer quelque chose
user2584960

92

J'aime utiliser SFINAEpour 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 Cest 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.


1
@Johannes Curieusement, GCC (4.8) et Clang (3.2) acceptent de déclarer des tableaux de taille 0 (donc le type n'est pas vraiment "invalide"), pourtant il se comporte correctement sur votre code. Il y a probablement un support spécial pour ce cas dans le cas de SFINAE par rapport aux utilisations "régulières" des types.
akim

@akim: si jamais cela est vrai (bizarre?! depuis quand?) alors peut M <= N ? 1 : -1- être pourrait fonctionner à la place.
v.oddou

1
@ v.oddou Essayez simplement 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 ).
akim le

@akim: ouais c'est ce que je pensais -> C99. Ce n'est pas autorisé en C ++, voici ce que vous obtenez avec un compilateur moderne:error C2466: cannot allocate an array of constant size 0
v.oddou

1
@ v.oddou Non, je voulais vraiment dire C ++, et en fait C ++ 11: clang ++ et g ++ l'acceptent, et j'ai pointé du doigt une page qui explique pourquoi c'est utile.
akim

16

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.


Votre premier bloc de code redéfinit le même modèle.
TC du

Puisqu'il n'y a aucun type pour lequel is_integral et is_floating_point sont tous les deux vrais, cela devrait être un soit ou parce que SFINAE en supprimera au moins un.
odinthenerd

Vous redéfinissez le même modèle avec différents arguments de modèle par défaut. Avez-vous essayé de le compiler?
TC

2
Je suis nouveau dans la métaprogrammation de modèles, donc je voulais comprendre cet exemple. Y a-t-il une raison pour laquelle vous utilisez 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&>?
Kevin Doyon

1
À propos de votre TypeSink, C ++ 17 ont std::void_t:)
YSC

10

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.


4

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_tet 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.


3

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 valuevaleur de s pour voir si Tc'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;
}

Que signifie cette syntaxe int C::*dans votre réponse? Comment peut C::*être un nom de paramètre?
Kirill Kobelev

1
C'est un pointeur vers un membre. Quelques références: isocpp.org/wiki/faq/pointers-to-members
whoan

@KirillKobelev int C::*est le type d'un pointeur vers une intvariable membre de C.
YSC

3

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

declvalest un utilitaire qui vous donne une "fausse référence" à un objet d'un type qui ne peut pas être facilement construit. declvalest 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';
}

0

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? )

https://godbolt.org/z/c2NmzR

#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' constexprastuce variable:

constexpr bool ispointer = is_function_pointer(var);
std::cout << "ispointer " << ispointer << std::endl;

Par le standard C ++, toutes les constexprvariables 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? ).


0

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
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.