std :: enable_if pour compiler conditionnellement une fonction membre


156

J'essaie de faire travailler un exemple simple pour comprendre comment l'utiliser std::enable_if. Après avoir lu cette réponse , j'ai pensé qu'il ne devrait pas être trop difficile de trouver un exemple simple. Je veux utiliser std::enable_ifpour choisir entre deux fonctions membres et n'autoriser qu'une seule d'entre elles à être utilisée.

Malheureusement, ce qui suit ne se compile pas avec gcc 4.7 et après des heures et des heures d'essais, je vous demande quelle est mon erreur.

#include <utility>
#include <iostream>

template< class T >
class Y {

    public:
        template < typename = typename std::enable_if< true >::type >
        T foo() {
            return 10;
        }
        template < typename = typename std::enable_if< false >::type >
        T foo() {
            return 10;
        }

};


int main() {
    Y< double > y;

    std::cout << y.foo() << std::endl;
}

gcc signale les problèmes suivants:

% LANG=C make CXXFLAGS="-std=c++0x" enable_if
g++ -std=c++0x    enable_if.cpp   -o enable_if
enable_if.cpp:12:65: error: `type' in `struct std::enable_if<false>' does not name a type
enable_if.cpp:13:15: error: `template<class T> template<class> T Y::foo()' cannot be overloaded
enable_if.cpp:9:15: error: with `template<class T> template<class> T Y::foo()'

Pourquoi g ++ ne supprime-t-il pas la mauvaise instanciation pour la deuxième fonction membre? Selon la norme, std::enable_if< bool, T = void >::typen'existe que lorsque le paramètre de modèle booléen est vrai. Mais pourquoi g ++ ne considère-t-il pas cela comme SFINAE? Je pense que le message d'erreur de surcharge vient du problème que g ++ ne supprime pas la fonction du deuxième membre et pense que cela devrait être une surcharge.


1
Je ne suis pas sûr, mais je pense que c'est le suivant: enable_if est basé sur SFINAE (l'échec de la substitution n'est pas une erreur). Cependant, vous n'avez aucune substitution ici, car aucun paramètre ne peut être utilisé pour déterminer la surcharge à utiliser. Vous devriez faire dépendre le "vrai" et le "faux" de T. (je sais que vous ne vouliez pas le faire dans l'exemple simple, mais c'est probablement trop simple maintenant ...)
Philipp

3
J'ai pensé à ça aussi et essayé de l'utiliser std::is_same< T, int >::valueet ! std::is_same< T, int >::valuequi donne le même résultat.
evnu

Réponses:


117

SFINAE ne fonctionne que si la substitution dans la déduction d'argument d'un argument modèle rend la construction mal formée. Il n'y a pas de telle substitution.

J'ai pensé à ça aussi et essayé de l'utiliser std::is_same< T, int >::valueet ! std::is_same< T, int >::valuequi donne le même résultat.

C'est parce que lorsque le modèle de classe est instancié (ce qui se produit lorsque vous créez un objet de type Y<int>parmi d'autres cas), il instancie toutes ses déclarations de membres (pas nécessairement leurs définitions / corps!). Parmi eux se trouvent également ses modèles de membres. Notez que Tc'est connu alors, et !std::is_same< T, int >::valuedonne faux. Il va donc créer une classe Y<int>qui contient

class Y<int> {
    public:
        /* instantiated from
        template < typename = typename std::enable_if< 
          std::is_same< T, int >::value >::type >
        T foo() {
            return 10;
        }
        */

        template < typename = typename std::enable_if< true >::type >
        int foo();

        /* instantiated from

        template < typename = typename std::enable_if< 
          ! std::is_same< T, int >::value >::type >
        T foo() {
            return 10;
        }
        */

        template < typename = typename std::enable_if< false >::type >
        int foo();
};

L' std::enable_if<false>::typeaccès à un type non existant, de sorte que la déclaration est mal formée. Et donc votre programme est invalide.

Vous devez faire en sorte que les modèles de membre enable_ifdépendent d'un paramètre du modèle de membre lui-même. Ensuite, les déclarations sont valides, car tout le type est toujours dépendant. Lorsque vous essayez d'appeler l'un d'eux, la déduction d'arguments pour leurs arguments de modèle se produit et SFINAE se produit comme prévu. Voir cette question et la réponse correspondante sur la façon de procéder.


14
... Juste pour clarifier, au cas où cela Yserait utile: lorsqu'une instance de la classe modèle est instanciée, le compilateur ne compilera pas réellement les fonctions membres du modèle; cependant, le compilateur effectuera la substitution de Tdans les DÉCLARATIONS de modèle de membre afin que ces modèles de membre puissent être instanciés ultérieurement. Ce point de défaillance n'est pas SFINAE, car SFINAE s'applique uniquement lors de la détermination de l'ensemble des fonctions possibles pour la résolution de surcharge , et l'instanciation d'une classe n'est pas un cas de détermination d'un ensemble de fonctions pour la résolution de surcharge. (Ou alors je pense!)
Dan Nissenbaum

93

J'ai fait ce petit exemple qui fonctionne également.

#include <iostream>
#include <type_traits>

class foo;
class bar;

template<class T>
struct is_bar
{
    template<class Q = T>
    typename std::enable_if<std::is_same<Q, bar>::value, bool>::type check()
    {
        return true;
    }

    template<class Q = T>
    typename std::enable_if<!std::is_same<Q, bar>::value, bool>::type check()
    {
        return false;
    }
};

int main()
{
    is_bar<foo> foo_is_bar;
    is_bar<bar> bar_is_bar;
    if (!foo_is_bar.check() && bar_is_bar.check())
        std::cout << "It works!" << std::endl;

    return 0;
}

Commentez si vous voulez que je développe. Je pense que le code est plus ou moins explicite, mais encore une fois, je l'ai fait pour que je me trompe peut-être :)

Vous pouvez le voir en action ici .


2
Cela ne compile pas sur VS2012. error C4519: default template arguments are only allowed on a class template.
PythonNut

1
C'est malheureux. Je l'ai testé uniquement avec gcc. Peut-être que cela aide: stackoverflow.com/a/17543296/660982
jpihl

1
c'est certainement la meilleure réponse ici et exactement ce que je cherchais.
Weipeng L

3
Pourquoi est-il nécessaire de créer une autre classe de modèle Q, même si elle est égale à T?
ilya1725

1
Parce que vous devez modéliser la testfonction membre. Les deux ne peuvent pas exister en même temps. Qtransfère simplement le type de modèle de classe T. Vous pouvez supprimer le modèle de classe Tcomme ceci: cpp.sh/4nxw mais cela va à l' encontre de l'objectif.
jpihl

13

Pour les retardataires qui recherchent une solution qui "fonctionne juste":

#include <utility>
#include <iostream>

template< typename T >
class Y {

    template< bool cond, typename U >
    using resolvedType  = typename std::enable_if< cond, U >::type; 

    public:
        template< typename U = T > 
        resolvedType< true, U > foo() {
            return 11;
        }
        template< typename U = T >
        resolvedType< false, U > foo() {
            return 12;
        }

};


int main() {
    Y< double > y;

    std::cout << y.foo() << std::endl;
}

Compiler avec:

g++ -std=gnu++14 test.cpp 

Courir donne:

./a.out 
11

6
Euh, pourquoi renommez-vous std::enable_if_ten resolvedType.
Qwertie

1
Parce que tout le monde ne peut pas utiliser C ++ 17 pour des raisons qui peuvent varier considérablement.
James Yang

9

De ce post:

Les arguments de modèle par défaut ne font pas partie de la signature d'un modèle

Mais on peut faire quelque chose comme ça:

#include <iostream>

struct Foo {
    template < class T,
               class std::enable_if < !std::is_integral<T>::value, int >::type = 0 >
    void f(const T& value)
    {
        std::cout << "Not int" << std::endl;
    }

    template<class T,
             class std::enable_if<std::is_integral<T>::value, int>::type = 0>
    void f(const T& value)
    {
        std::cout << "Int" << std::endl;
    }
};

int main()
{
    Foo foo;
    foo.f(1);
    foo.f(1.1);

    // Output:
    // Int
    // Not int
}

Cela fonctionne, mais il s'agit essentiellement de modèles de fonctions, pas de la classe elle-même ... Cela ne permet pas non plus de supprimer l'une des deux fonctions prototypées de manière identique (lorsque vous devez passer la surcharge). Cependant, l'idée est belle. Pourriez-vous réécrire l'exemple OP sous une forme fonctionnelle, s'il vous plaît?
user1284631

5

Une façon de résoudre ce problème, la spécialisation des fonctions membres est de placer la spécialisation dans une autre classe, puis d'hériter de cette classe. Vous devrez peut-être modifier l'ordre d'héritage pour accéder à toutes les autres données sous-jacentes, mais cette technique fonctionne.

template< class T, bool condition> struct FooImpl;
template<class T> struct FooImpl<T, true> {
T foo() { return 10; }
};

template<class T> struct FoolImpl<T,false> {
T foo() { return 5; }
};

template< class T >
class Y : public FooImpl<T, boost::is_integer<T> > // whatever your test is goes here.
{
public:
    typedef FooImpl<T, boost::is_integer<T> > inherited;

    // you will need to use "inherited::" if you want to name any of the 
    // members of those inherited classes.
};

L'inconvénient de cette technique est que si vous avez besoin de tester beaucoup de choses différentes pour différentes fonctions membres, vous devrez créer une classe pour chacune et l'enchaîner dans l'arborescence d'héritage. Cela est vrai pour accéder aux membres de données communs.

Ex:

template<class T, bool condition> class Goo;
// repeat pattern above.

template<class T, bool condition>
class Foo<T, true> : public Goo<T, boost::test<T> > {
public:
    typedef Goo<T, boost::test<T> > inherited:
    // etc. etc.
};

4

Le booléen doit dépendre du paramètre de modèle déduit. Donc, un moyen simple de corriger est d'utiliser un paramètre booléen par défaut:

template< class T >
class Y {

    public:
        template < bool EnableBool = true, typename = typename std::enable_if<( std::is_same<T, double>::value && EnableBool )>::type >
        T foo() {
            return 10;
        }

};

Cependant, cela ne fonctionnera pas si vous souhaitez surcharger la fonction membre. Au lieu de cela, il est préférable d'utiliser TICK_MEMBER_REQUIRESla bibliothèque Tick :

template< class T >
class Y {

    public:
        TICK_MEMBER_REQUIRES(std::is_same<T, double>::value)
        T foo() {
            return 10;
        }

        TICK_MEMBER_REQUIRES(!std::is_same<T, double>::value)
        T foo() {
            return 10;
        }

};

Vous pouvez également implémenter votre propre macro requiert une macro comme celle-ci (juste au cas où vous ne voudriez pas utiliser une autre bibliothèque):

template<long N>
struct requires_enum
{
    enum class type
    {
        none,
        all       
    };
};


#define MEMBER_REQUIRES(...) \
typename requires_enum<__LINE__>::type PrivateRequiresEnum ## __LINE__ = requires_enum<__LINE__>::type::none, \
class=typename std::enable_if<((PrivateRequiresEnum ## __LINE__ == requires_enum<__LINE__>::type::none) && (__VA_ARGS__))>::type

Cela n'a pas fonctionné pour moi de cette façon. Maaybe quelque chose manque? Pourriez-vous réécrire l'exemple OP sous une forme fonctionnelle, s'il vous plaît?
user1284631

L'exemple d'origine ne fonctionne pas avec la surcharge. J'ai mis à jour ma réponse comment vous pouvez le faire avec une surcharge.
Paul Fultz II

0

Voici mon exemple minimaliste, utilisant une macro. Utilisez des crochets doubles enable_if((...))lorsque vous utilisez des expressions plus complexes.

template<bool b, std::enable_if_t<b, int> = 0>
using helper_enable_if = int;

#define enable_if(value) typename = helper_enable_if<value>

struct Test
{
     template<enable_if(false)>
     void run();
}
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.