std :: enable_if para compilar condicionalmente una función miembro

156

Estoy tratando de obtener un ejemplo simple para entender cómo usarlo std::enable_if. Después de leer esta respuesta , pensé que no debería ser demasiado difícil encontrar un ejemplo simple. Quiero usar std::enable_ifpara elegir entre dos funciones miembro y permitir que solo se use una de ellas.

Desafortunadamente, lo siguiente no se compila con gcc 4.7 y después de horas y horas de intentarlo, les pregunto cuál es mi error.

#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 informa los siguientes 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 qué g ++ no elimina la instanciación incorrecta para la función del segundo miembro? Según el estándar, std::enable_if< bool, T = void >::typesolo existe cuando el parámetro de la plantilla booleana es verdadero. Pero, ¿por qué g ++ no considera esto como SFINAE? Creo que el mensaje de error de sobrecarga proviene del problema de que g ++ no elimina la función del segundo miembro y cree que esto debería ser una sobrecarga.

evnu
fuente
1
No estoy seguro, pero creo que es lo siguiente: enable_if se basa en SFINAE (el error de sustitución no es un error). Sin embargo, no tiene ninguna sustitución aquí, porque no se puede usar ningún parámetro para determinar qué sobrecarga usar. Debería hacer que el "verdadero" y el "falso" dependan de T. (Sé que no quería hacerlo en el ejemplo simple, pero probablemente ahora sea demasiado simple ...)
Philipp
3
Pensé en eso también y trató de utilizar std::is_same< T, int >::valuey ! std::is_same< T, int >::valueque le da el mismo resultado.
evnu

Respuestas:

117

SFINAE solo funciona si la sustitución en la deducción de argumentos de un argumento plantilla hace que el constructo esté mal formado. No hay tal sustitución.

Pensé en eso también y trató de utilizar std::is_same< T, int >::valuey ! std::is_same< T, int >::valueque le da el mismo resultado.

Esto se debe a que cuando se crea una instancia de la plantilla de clase (que ocurre cuando crea un objeto de tipo, Y<int>entre otros casos), crea una instancia de todas sus declaraciones de miembros (¡no necesariamente sus definiciones / cuerpos!). Entre ellos también se encuentran sus plantillas de miembros. Tenga en cuenta que Tse sabe entonces, y !std::is_same< T, int >::valueproduce falso. Entonces creará una clase Y<int>que contiene

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

El std::enable_if<false>::typeacceso a un tipo no-existente, por lo que se forma mal esa declaración. Y así su programa no es válido.

Debe hacer que las plantillas de miembros enable_ifdependan de un parámetro de la plantilla de miembros en sí. Entonces las declaraciones son válidas, porque todo el tipo sigue siendo dependiente. Cuando intenta llamar a uno de ellos, la deducción de argumentos para sus argumentos de plantilla ocurre y SFINAE ocurre como se esperaba. Vea esta pregunta y la respuesta correspondiente sobre cómo hacerlo.

Johannes Schaub - litb
fuente
14
... Solo para aclarar, en caso de que sea útil: cuando se instancia una instancia de la Yclase de plantilla, el compilador en realidad no compilará las funciones miembro de la plantilla; sin embargo, el compilador REALIZARÁ la sustitución de Tlas DECLARACIONES de plantilla de miembro para que estas plantillas de miembro puedan instanciarse en un momento posterior. Este punto de falla no es SFINAE, porque SFINAE solo se aplica al determinar el conjunto de funciones posibles para la resolución de sobrecarga , y crear instancias de una clase no es un caso de determinar un conjunto de funciones para la resolución de sobrecarga. (¡O eso creo!)
Dan Nissenbaum el
93

Hice este breve ejemplo que también 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;
}

Comenta si quieres que explique. Creo que el código se explica más o menos por sí mismo, pero de nuevo lo hice para estar equivocado :)

Puedes verlo en acción aquí .

jpihl
fuente
2
Esto no se compila en VS2012. error C4519: default template arguments are only allowed on a class template.
PythonNut
1
Eso es lamentable. Solo lo probé con gcc. Quizás esto ayude: stackoverflow.com/a/17543296/660982
jpihl
1
Esta es sin duda la mejor respuesta aquí y exactamente lo que estaba buscando.
Weipeng L
3
¿Por qué es necesario crear otra clase de plantilla Q, aunque sea igual a T?
ilya1725
1
Porque necesita modelar la testfunción miembro. Ambos no pueden existir al mismo tiempo. Qsolo reenvía el tipo de plantilla de clase T. Puede eliminar la plantilla de clase de la siguiente Tmanera: cpp.sh/4nxw, pero eso frustra el propósito.
jpihl
13

Para aquellos recién llegados que buscan una solución que "simplemente 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;
}

Compilar con:

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

Correr da:

./a.out 
11
usuario1284631
fuente
66
Um, ¿por qué cambiar el nombre std::enable_if_ta resolvedType.
Qwertie
1
Porque no todos pueden usar C ++ 17 por razones que pueden variar ampliamente.
James Yang
9

De esta publicación:

Los argumentos de plantilla predeterminados no son parte de la firma de una plantilla

Pero uno puede hacer algo como esto:

#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
fuente
Funciona, pero esto es básicamente funciones de plantilla, no la clase en sí ... Tampoco permite eliminar una de las dos funciones prototípicas idénticas (cuando necesita pasar por sobrecarga). Sin embargo, la idea es buena. ¿Podría reescribir el ejemplo de OP en un formulario de trabajo, por favor?
user1284631
5

Una forma de resolver este problema, la especialización de las funciones miembro es colocar la especialización en otra clase, luego heredar de esa clase. Puede que tenga que cambiar el orden de herencia para obtener acceso a todos los demás datos subyacentes, pero esta 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.
};

La desventaja de esta técnica es que si necesita probar muchas cosas diferentes para diferentes funciones miembro, tendrá que hacer una clase para cada una y encadenarla en el árbol de herencia. Esto es cierto para acceder a miembros de datos comunes.

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
fuente
4

El booleano debe depender del parámetro de plantilla que se deduce. Entonces, una manera fácil de arreglarlo es usar un parámetro booleano predeterminado:

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

};

Sin embargo, esto no funcionará si desea sobrecargar la función miembro. En cambio, es mejor usarlo TICK_MEMBER_REQUIRESdesde la 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;
        }

};

También puede implementar su propio miembro requiere una macro como esta (en caso de que no quiera usar otra 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
fuente
No me funcionó de esa manera. Tal vez algo falta? ¿Podría reescribir el ejemplo de OP en un formulario de trabajo, por favor?
user1284631
El ejemplo original no funciona con sobrecarga. Actualicé mi respuesta sobre cómo puedes hacerlo con la sobrecarga.
Paul Fultz II
0

Aquí está mi ejemplo minimalista, usando una macro. Use corchetes dobles enable_if((...))cuando use expresiones más complejas.

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
fuente