Przykłady C ++ SFINAE?

123

Chcę zająć się bardziej metaprogramowaniem szablonów. Wiem, że SFINAE oznacza „niepowodzenie zamiany nie jest błędem”. Ale czy ktoś może mi pokazać dobre zastosowanie SFINAE?

rlbond
źródło
2
To jest dobre pytanie. Rozumiem SFINAE całkiem dobrze, ale nie sądzę, żebym kiedykolwiek musiał go używać (chyba że biblioteki robią to bez mojej wiedzy).
Zifre
5
STL ujął to nieco inaczej w często zadawanych pytaniach : „Awaria zastępowania nie jest słoniem”
vulcan raven

Odpowiedzi:

73

Oto jeden przykład ( stąd ):

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

Gdy IsClassT<int>::Yesjest oceniane, nie można przekonwertować wartości 0, int int::*ponieważ int nie jest klasą, więc nie może mieć wskaźnika elementu członkowskiego. Gdyby SFINAE nie istniało, wystąpiłby błąd kompilatora, coś w rodzaju „0 nie może zostać przekonwertowane na wskaźnik elementu członkowskiego dla typu int niebędącego klasą”. Zamiast tego po prostu używa ...formularza, który zwraca Two, a zatem zwraca wartość false, int nie jest typem klasy.

Greg Rogers
źródło
8
@rlbond, odpowiedziałem na twoje pytanie w komentarzach do tego pytania tutaj: stackoverflow.com/questions/822059/… . W skrócie: jeśli obie funkcje testowe są kandydatami i wykonalne, wówczas „...” ma najgorszy koszt konwersji i dlatego nigdy nie zostanie przejęty na korzyść drugiej funkcji. "..." to wielokropek, zmienna-arg: int printf (stała char *, ...);
Johannes Schaub - litb
21
Najdziwniejszą rzeczą tutaj IMO nie jest ..., ale raczej to int C::*, czego nigdy nie widziałem i musiałem iść spojrzeć w górę. Znalazłem odpowiedź na pytanie, co to jest i do czego może być używane tutaj: stackoverflow.com/questions/670734/ ...
HostileFork mówi nie ufaj SE
1
czy ktoś może wyjaśnić, czym jest C :: *? Czytałem wszystkie komentarze i linki, ale wciąż się zastanawiam, int C :: * oznacza, że ​​jest to wskaźnik elementu członkowskiego typu int. co jeśli klasa nie ma elementu typu int? czego mi brakuje? i jak działa na to test <T> (0)? Pewnie czegoś brakuje
user2584960
92

Lubię używać SFINAEdo sprawdzania warunków boolowskich.

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 */
}

To może być całkiem przydatne. Na przykład użyłem go do sprawdzenia, czy lista inicjatorów zebrana za pomocą przecinka operatora nie jest dłuższa niż ustalony rozmiar

template<int N>
struct Vector {
    template<int M> 
    Vector(MyInitList<M> const& i, char(*)[M <= N] = 0) { /* ... */ }
}

Lista jest akceptowana tylko wtedy, gdy M jest mniejsze niż N, co oznacza, że ​​lista inicjalizacyjna nie zawiera zbyt wielu elementów.

Składnia char(*)[C]oznacza: Wskaźnik do tablicy z typem elementu char i rozmiarem C. Jeśli Cjest fałszywe (tutaj 0), to otrzymujemy nieprawidłowy typ char(*)[0], wskaźnik do tablicy o zerowym rozmiarze: SFINAE sprawia, że ​​szablon zostanie wówczas zignorowany.

Wyrażone za pomocą boost::enable_if, wygląda to tak

template<int N>
struct Vector {
    template<int M> 
    Vector(MyInitList<M> const& i, 
           typename enable_if_c<(M <= N)>::type* = 0) { /* ... */ }
}

W praktyce często uważam, że sprawdzanie warunków jest użyteczną umiejętnością.

Johannes Schaub - litb
źródło
1
@Johannes Co dziwne, GCC (4.8) i Clang (3.2) akceptują deklarowanie tablic o rozmiarze 0 (więc typ nie jest tak naprawdę „nieprawidłowy”), ale zachowuje się poprawnie w twoim kodzie. Prawdopodobnie istnieje specjalne wsparcie dla tego przypadku w przypadku SFINAE w porównaniu z „zwykłymi” zastosowaniami typów.
akim
@akim: jeśli to kiedykolwiek jest prawdą (dziwne?! od kiedy?), to może M <= N ? 1 : -1mogłoby zadziałać.
v.oddou,
1
@ v.oddou Po prostu spróbuj int foo[0]. Nie dziwię się, że jest obsługiwany, ponieważ pozwala na bardzo przydatną sztuczkę „struct kończąca się tablicą o długości 0” ( gcc.gnu.org/onlinedocs/gcc/Zero-Length.html ).
akim
@akim: tak, to jest to, co myślałem -> C99. To nie jest dozwolone w C ++, oto co otrzymujesz dzięki nowoczesnemu kompilatorowi:error C2466: cannot allocate an array of constant size 0
v.oddou
1
@ v.oddou Nie, naprawdę miałem na myśli C ++, a właściwie C ++ 11: akceptują to zarówno clang ++, jak i g ++, i wskazałem na stronę, która wyjaśnia, dlaczego jest to przydatne.
akim
16

W C ++ 11 testy SFINAE stały się znacznie ładniejsze. Oto kilka przykładów typowych zastosowań:

Wybierz przeciążenie funkcji w zależności od cech

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
}

Używając tak zwanego idiomu typu sink można przeprowadzić całkiem dowolne testy na typie, takie jak sprawdzenie, czy ma on element członkowski i czy ten element członkowski jest określonego typu

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

Oto przykład na żywo: http://ideone.com/dHhyHE Niedawno napisałem też całą sekcję o SFINAE i wysyłaniu tagów na moim blogu (bezwstydna wtyczka, ale odpowiednia) http://metaporky.blogspot.de/2014/08/ część-7-statyczna-wysyłka-function.html

Zauważ, że od C ++ 14 istnieje std :: void_t, który jest zasadniczo taki sam jak mój TypeSink tutaj.

odinthenerd
źródło
Twój pierwszy blok kodu na nowo definiuje ten sam szablon.
TC
Ponieważ nie ma typu, dla którego is_integral i is_floating_point są prawdziwe, powinno to być albo albo, ponieważ SFINAE usunie co najmniej jeden.
odinthenerd
Ponownie definiujesz ten sam szablon z różnymi domyślnymi argumentami szablonu. Czy próbowałeś go skompilować?
TC
2
Nie mam doświadczenia w metaprogramowaniu szablonów, więc chciałem zrozumieć ten przykład. Czy jest jakiś powód, dla którego używasz TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>w jednym miejscu, a potem TypeSinkT<decltype(&T::bar)>w innym? Czy jest to &konieczne std::declval<T&>?
Kevin Doyon
1
O twoim TypeSink, C ++ 17 mają std::void_t:)
YSC
10

Biblioteka enable_if firmy Boost oferuje ładny, czysty interfejs do korzystania z SFINAE. Jeden z moich ulubionych przykładów użycia znajduje się w bibliotece Boost.Iterator . SFINAE służy do włączania konwersji typu iteratora.

David Joyner
źródło
4

C ++ 17 prawdopodobnie zapewni ogólne metody zapytań o funkcje. Aby uzyskać szczegółowe informacje, patrz N4502 , ale jako samodzielny przykład rozważ poniższe.

Ta część jest częścią stałą, umieść ją w nagłówku.

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

Poniższy przykład, zaczerpnięty z N4502 , pokazuje użycie:

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

W porównaniu z innymi implementacjami ta jest dość prosta: wystarczy zredukowany zestaw narzędzi ( void_ti detect). Poza tym zgłoszono (patrz N4502 ), że jest on mierzalnie bardziej wydajny (czas kompilacji i zużycie pamięci kompilatora) niż poprzednie podejścia.

Oto przykład na żywo , który obejmuje poprawki przenośności dla GCC w wersji wcześniejszej niż 5.1.

akim
źródło
3

Oto kolejny (późno) SFINAE przykład, na podstawie Greg Rogers „s odpowiedź :

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

W ten sposób możesz sprawdzić valuewartość, aby zobaczyć, czy Tjest to klasa, czy nie:

int main(void) {
    std::cout << IsClassT<std::string>::value << std::endl; // true
    std::cout << IsClassT<int>::value << std::endl;         // false
    return 0;
}
whoan
źródło
Co oznacza ta składnia int C::*w Twojej odpowiedzi? Jak może C::*być nazwą parametru?
Kirill Kobelev,
1
To wskaźnik do członka. Niektóre źródła
whoan
@KirillKobelev int C::*to typ wskaźnika do intzmiennej składowej C.
YSC
3

Oto jeden dobry artykuł z SFINAE: Wprowadzenie do koncepcji SFINAE w C ++: introspekcja członka klasy w czasie kompilacji .

Podsumuj to w następujący sposób:

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

declvalto narzędzie, które daje „fałszywe odniesienie” do obiektu typu, którego nie można łatwo skonstruować. declvaljest bardzo przydatny w naszych konstrukcjach 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';
}
zangw
źródło
0

Tutaj używam przeciążenia funkcji szablonu (nie bezpośrednio SFINAE), aby określić, czy wskaźnik jest wskaźnikiem funkcji lub klasy elementu członkowskiego: ( Czy można naprawić wskaźniki funkcji elementu członkowskiego iostream cout / cerr drukowane jako 1 lub prawda? )

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

Wydruki

0. false
1. true
2. true
3. true
4. true

W istocie kod mógłby (w zależności od "dobrej" woli kompilatora) wygenerować wywołanie funkcji, która zwróci prawdę lub fałsz w czasie wykonywania. Jeśli chcesz wymusić is_function_pointer(var)ocenę w typie kompilacji (żadne wywołania funkcji nie są wykonywane w czasie wykonywania), możesz użyć constexprzmiennej sztuczki:

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

Standard C ++ constexprgwarantuje , że wszystkie zmienne zostaną ocenione w czasie kompilacji ( Obliczanie długości łańcucha w języku C w czasie kompilacji. Czy to naprawdę jest constexpr? ).

użytkownik
źródło
0

Poniższy kod używa SFINAE, aby umożliwić kompilatorowi wybranie przeciążenia na podstawie tego, czy typ ma określoną metodę, czy nie:

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

Wynik:

Pływak: 1
Int: -1
kowboj
źródło