std :: enable_if, aby warunkowo skompilować funkcję składową

156

Próbuję uzyskać prosty przykład do pracy, aby zrozumieć, jak używać std::enable_if. Po przeczytaniu tej odpowiedzi pomyślałem, że nie powinno być trudno znaleźć prosty przykład. Chcę użyćstd::enable_if do wyboru między dwiema funkcjami składowymi i pozwolić na użycie tylko jednej z nich.

Niestety, poniższe nie skompiluje się z gcc 4.7 i po wielu godzinach prób, pytam was, jaki jest mój błąd.

#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 zgłasza następujące problemy:

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

Dlaczego g ++ nie usuwa niewłaściwego wystąpienia drugiej funkcji składowej? Zgodnie ze standardem std::enable_if< bool, T = void >::typeistnieje tylko wtedy, gdy parametr szablonu logicznego ma wartość true. Ale dlaczego g ++ nie uważa tego za SFINAE? Myślę, że komunikat o przeciążeniu pochodzi z problemu, że g ++ nie usuwa drugiej funkcji składowej i uważa, że ​​powinno to być przeciążenie.

evnu
źródło
1
Nie jestem pewien, ale wydaje mi się, że jest tak: enable_if jest oparty na SFINAE (niepowodzenie zamiany nie jest błędem). Jednak nie masz tutaj żadnego podstawienia, ponieważ nie można użyć żadnego parametru do określenia, którego przeciążenia użyć. Należy uczynić „true” und „false” zależą T. (wiem, że nie chcesz, aby to zrobić w prosty przykład, ale to chyba zbyt proste teraz ...)
Philipp
3
Myślałem, że zbyt i próbował użyć std::is_same< T, int >::valuei ! std::is_same< T, int >::valueco daje ten sam rezultat.
evnu

Odpowiedzi:

117

SFINAE działa tylko wtedy, gdy podstawienie w dedukcji argumentu z argumentu szablonu powoduje, że konstrukcja jest źle sformułowana. Nie ma takiej zamiany.

Myślałem, że zbyt i próbował użyć std::is_same< T, int >::valuei ! std::is_same< T, int >::valueco daje ten sam rezultat.

Dzieje się tak, ponieważ kiedy tworzony jest szablon klasy (co ma miejsce, gdy tworzysz obiekt typu Y<int>między innymi przypadkami), tworzy on instancję wszystkich swoich deklaracji składowych (niekoniecznie ich definicji / treści!). Wśród nich są także szablony członków. Zauważ, że Tjest to znane i !std::is_same< T, int >::valuedaje fałsz. Więc utworzy klasę, Y<int>która zawiera

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

Plik std::enable_if<false>::typeDostęp non-istniejący typ, tak że deklaracja jest źle sformułowane. A zatem twój program jest nieprawidłowy.

Musisz uzależnić szablony elementów członkowskich enable_ifod parametru samego szablonu elementu członkowskiego. Wtedy deklaracje są ważne, ponieważ cały typ jest nadal zależny. Kiedy próbujesz zadzwonić do jednego z nich, następuje dedukcja argumentów dla ich argumentów szablonu i SFINAE dzieje się zgodnie z oczekiwaniami. Zobacz to pytanie i odpowiadającą mu odpowiedź, jak to zrobić.

Johannes Schaub - litb
źródło
14
... Żeby wyjaśnić, na wypadek, gdyby było to przydatne: kiedy Ytworzona jest instancja klasy szablonu, kompilator w rzeczywistości nie skompiluje funkcji składowych szablonu; jednak kompilator WYKONUJE podstawienie Tdo szablonu DECLARATIONS, tak aby te szablony członkowskie mogły zostać utworzone w późniejszym czasie. Ten punkt awarii nie jest SFINAE, ponieważ SFINAE ma zastosowanie tylko podczas określania zestawu możliwych funkcji do rozwiązania przeciążenia , a tworzenie wystąpienia klasy nie jest przypadkiem określania zestawu funkcji do rozwiązania przeciążenia. (A przynajmniej tak mi się wydaje!)
Dan Nissenbaum
93

Zrobiłem ten krótki przykład, który również działa.

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

Skomentuj, jeśli chcesz, żebym to rozwinął. Myślę, że kod jest mniej więcej oczywisty, ale z drugiej strony zrobiłem to, więc mogę się mylić :)

Możesz zobaczyć to w akcji tutaj .

jpihl
źródło
2
Nie kompiluje się na VS2012. error C4519: default template arguments are only allowed on a class template.
PythonNut
1
To niefortunne. Testowałem to tylko z gcc. Może to pomoże: stackoverflow.com/a/17543296/660982
jpihl
1
to z pewnością najlepsza odpowiedź tutaj i dokładnie to, czego szukałem.
Weipeng L
3
Dlaczego istnieje potrzeba tworzenia kolejnej klasy szablonu Q, mimo że jest równa T?
ilya1725
1
Ponieważ musisz utworzyć szablon testfunkcji członkowskiej. Obie nie mogą istnieć w tym samym czasie. Qpo prostu przekazuje typ szablonu klasy T. Możesz usunąć szablon klasy w następujący Tsposób: cpp.sh/4nxw, ale to trochę mija się z celem.
jpihl
13

Dla tych spóźnionych, którzy szukają rozwiązania, które „po prostu działa”:

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

Połącz z:

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

Bieganie daje:

./a.out 
11
user1284631
źródło
6
Um, dlaczego miałbyś zmienić nazwę std::enable_if_tna resolvedType.
Qwertie
1
Ponieważ nie każdy może używać C ++ 17 z powodów, które mogą być bardzo różne.
James Yang
9

Z tego postu:

Domyślne argumenty szablonu nie są częścią podpisu szablonu

Ale można zrobić coś takiego:

#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
źródło
Działa, ale to w zasadzie funkcje szablonów, a nie sama klasa ... Nie pozwala też na porzucenie jednej z dwóch identycznie prototypowanych funkcji (gdy trzeba przejść przez przeciążenie). Jednak pomysł jest fajny. Czy mógłbyś przepisać przykład OP w działającej formie?
user1284631
5

Jednym ze sposobów rozwiązania tego problemu, specjalizacją funkcji składowych, jest umieszczenie specjalizacji w innej klasie, a następnie dziedziczenie z tej klasy. Być może trzeba będzie zmienić kolejność dziedziczenia, aby uzyskać dostęp do wszystkich innych danych bazowych, ale ta technika działa.

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

Wadą tej techniki jest to, że jeśli chcesz przetestować wiele różnych rzeczy dla różnych funkcji składowych, musisz utworzyć klasę dla każdej z nich i połączyć ją w drzewo dziedziczenia. Dotyczy to dostępu do wspólnych członków danych.

Dawny:

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
źródło
4

Wartość logiczna musi zależeć od wydedukowanego parametru szablonu. Tak więc prostym sposobem na naprawienie jest użycie domyślnego parametru boolowskiego:

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

};

Jednak to nie zadziała, jeśli chcesz przeciążać funkcję składową. Zamiast tego najlepiej jest używać TICK_MEMBER_REQUIRESz biblioteki 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;
        }

};

Możesz również zaimplementować własne makra członkowskie, takie jak to (na wypadek, gdybyś nie chciał używać innej biblioteki):

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
źródło
To mi się nie udało. Może czegoś brakuje? Czy mógłbyś przepisać przykład OP w działającej formie?
user1284631
Oryginalny przykład nie działa z przeciążeniem. Zaktualizowałem moją odpowiedź, jak możesz to zrobić z przeciążeniem.
Paul Fultz II
0

Oto mój minimalistyczny przykład z użyciem makra. Użyj podwójnych nawiasów, enable_if((...))gdy używasz bardziej złożonych wyrażeń.

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
źródło