std :: enable_if para compilar condicionalmente uma função de membro

156

Estou tentando obter um exemplo simples para entender como usar std::enable_if. Depois de ler esta resposta , pensei que não seria muito difícil criar um exemplo simples. Eu quero usar std::enable_ifpara escolher entre duas funções-membro e permitir que apenas uma delas seja usada.

Infelizmente, o seguinte não é compilado com o gcc 4.7 e, após horas e horas de tentativas, estou perguntando a vocês qual é o meu erro.

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

O gcc relata os seguintes problemas:

% 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()'

Por que o g ++ não exclui a instanciação incorreta para a segunda função de membro? De acordo com o padrão, std::enable_if< bool, T = void >::typesó existe quando o parâmetro do modelo booleano é verdadeiro. Mas por que o g ++ não considera isso como SFINAE? Eu acho que a mensagem de erro de sobrecarga vem do problema de que o g ++ não exclui a segunda função de membro e acredita que isso deve ser uma sobrecarga.

evnu
fonte
1
Não tenho certeza, mas acho que é o seguinte: enable_if é baseado em SFINAE (falha na substituição não é um erro). No entanto, você não tem nenhuma substituição aqui, porque nenhum parâmetro não pode ser usado para determinar qual sobrecarga usar. Você deve fazer o "verdadeiro" und "false" dependem de T. (eu sei que você não queria fazê-lo no exemplo simples, mas é provavelmente muito simples agora ...)
Philipp
3
Pensei nisso também e tentei usar std::is_same< T, int >::valuee ! std::is_same< T, int >::valueque dá o mesmo resultado.
evnu

Respostas:

117

SFINAE só funciona se a substituição na dedução de argumento de um argumento de modelo tornar a construção mal formada. Não existe essa substituição.

Eu pensei nisso também e tentei usar std::is_same< T, int >::valuee o ! std::is_same< T, int >::valueque dá o mesmo resultado.

Isso ocorre porque quando o modelo de classe é instanciado (o que acontece quando você cria um objeto de tipo Y<int>entre outros casos), ele instancia todas as suas declarações de membros (não necessariamente suas definições / corpos!). Entre eles também estão os modelos de membros. Note que Té conhecido então e !std::is_same< T, int >::valueproduz false. Por isso, criará uma classe Y<int>que contém

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

O std::enable_if<false>::typeacessa um tipo inexistente, para que a declaração seja mal formada. E, portanto, seu programa é inválido.

Você precisa fazer com que os modelos de membros enable_ifdependam de um parâmetro do próprio modelo de membro. Em seguida, as declarações são válidas, porque todo o tipo ainda é dependente. Quando você tenta chamar um deles, a dedução de argumento para seus argumentos de modelo acontece e SFINAE acontece como esperado. Veja esta pergunta e a resposta correspondente sobre como fazer isso.

Johannes Schaub - litb
fonte
14
... Apenas para esclarecer, caso seja útil: Quando uma instância da Yclasse de modelo é instanciada, o compilador não compila realmente as funções de membro do modelo; no entanto, o compilador executará a substituição Tno modelo de membro DECLARATIONS para que esses modelos de membro possam ser instanciados posteriormente. Esse ponto de falha não é o SFINAE, porque o SFINAE se aplica apenas ao determinar o conjunto de funções possíveis para a resolução de sobrecarga , e instanciar uma classe não é o caso de determinar um conjunto de funções para a resolução de sobrecarga. (Ou então eu acho!)
Dan Nissenbaum
93

Fiz este pequeno exemplo que também funciona.

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

Comente se você quer que eu elabore. Eu acho que o código é mais ou menos auto-explicativo, mas, novamente, eu fiz isso para que eu possa estar errado :)

Você pode vê-lo em ação aqui .

jpihl
fonte
2
Isso não compila no VS2012. error C4519: default template arguments are only allowed on a class template.
precisa saber é o seguinte
1
Isso é lamentável. Eu só testei com o gcc. Talvez isso ajude: stackoverflow.com/a/17543296/660982
jpihl
1
esta é certamente a melhor resposta aqui e exatamente o que eu estava procurando.
Weipeng L
3
Por que é necessário criar outra classe de modelo Q, mesmo que seja igual a T?
precisa saber é o seguinte
1
Porque você precisa modelar a testfunção de membro. Ambos não podem existir ao mesmo tempo. Qapenas encaminha o tipo de modelo de classe T. Você pode remover o modelo de classe da seguinte Tmaneira: cpp.sh/4nxw, mas isso meio que derrota o objetivo.
jpihl
13

Para quem chega atrasado e procura uma solução que "simplesmente funcione":

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

Ajuntar com:

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

A corrida oferece:

./a.out 
11
user1284631
fonte
6
Hum, por que você iria mudar o nome std::enable_if_tpara resolvedType.
9119 Qwertie
1
Porque nem todos podem usar o C ++ 17 por razões que podem variar bastante.
James Yang
9

A partir deste post:

Os argumentos padrão do modelo não fazem parte da assinatura de um modelo

Mas pode-se fazer algo assim:

#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
}
Janek Olszak
fonte
Funciona, mas trata-se basicamente de funções de modelagem, não da própria classe ... Não permite a exclusão de uma das duas funções com prototipagem idêntica (quando você precisa passar por sobrecarga). No entanto, a ideia é boa. Você poderia reescrever o exemplo do OP em um formulário de trabalho, por favor?
user1284631
5

Uma maneira de resolver esse problema, a especialização de funções-membro é colocar a especialização em outra classe e depois herdá-la. Pode ser necessário alterar a ordem da herança para obter acesso a todos os outros dados subjacentes, mas essa técnica funciona.

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

A desvantagem dessa técnica é que, se você precisar testar muitas coisas diferentes para diferentes funções-membro, precisará criar uma classe para cada uma e encadear na árvore de herança. Isso é verdade para acessar membros de dados comuns.

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.
};
Gary Powell
fonte
4

O booleano precisa depender do parâmetro do modelo que está sendo deduzido. Portanto, uma maneira fácil de corrigir é usar um parâmetro booleano padrão:

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

};

No entanto, isso não funcionará se você deseja sobrecarregar a função de membro. Em vez disso, é melhor usar TICK_MEMBER_REQUIRESda biblioteca 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;
        }

};

Você também pode implementar seu próprio membro requer macro como esta (caso você não queira usar outra biblioteca):

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
Paul Fultz II
fonte
Não funcionou para mim dessa maneira. Talvez algo esteja faltando? Você poderia reescrever o exemplo do OP em um formulário de trabalho, por favor?
user1284631
O exemplo original não funciona com sobrecarga. Eu atualizei minha resposta como você pode fazer isso com sobrecarga.
Paul Fultz II
0

Aqui está o meu exemplo minimalista, usando uma macro. Use colchetes duplos enable_if((...))ao usar expressões mais complexas.

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();
}
Aedoro
fonte