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?
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?
Odpowiedzi:
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>::Yes
jest 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.
...
, 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/ ...
Lubię używać SFINAE
do 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 C
jest 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ą.
M <= N ? 1 : -1
mogłoby zadziałać.
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 ).
error C2466: cannot allocate an array of constant size 0
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.
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&>
?
TypeSink
, C ++ 17 mają std::void_t
:)
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.
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_t
i 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.
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ć value
wartość, aby zobaczyć, czy T
jest 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;
}
int C::*
w Twojej odpowiedzi? Jak może C::*
być nazwą parametru?
int C::*
to typ wskaźnika do int
zmiennej składowej C
.
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);
}
declval
to narzędzie, które daje „fałszywe odniesienie” do obiektu typu, którego nie można łatwo skonstruować. declval
jest 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';
}
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? )
#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ć constexpr
zmiennej sztuczki:
constexpr bool ispointer = is_function_pointer(var);
std::cout << "ispointer " << ispointer << std::endl;
Standard C ++ constexpr
gwarantuje , ż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? ).
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