Ich möchte mehr über die Meta-Programmierung von Vorlagen erfahren. Ich weiß, dass SFINAE für "Substitutionsfehler ist kein Fehler" steht. Aber kann mir jemand eine gute Verwendung für SFINAE zeigen?
Ich möchte mehr über die Meta-Programmierung von Vorlagen erfahren. Ich weiß, dass SFINAE für "Substitutionsfehler ist kein Fehler" steht. Aber kann mir jemand eine gute Verwendung für SFINAE zeigen?
Antworten:
Hier ist ein Beispiel ( von hier ):
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 };
};
Wenn IsClassT<int>::Yes
ausgewertet wird, kann 0 nicht konvertiert werden, int int::*
da int keine Klasse ist und daher keinen Elementzeiger haben kann. Wenn SFINAE nicht vorhanden wäre, würde ein Compilerfehler angezeigt, z. B. "0 kann nicht in Mitgliedszeiger für Nicht-Klassentyp int konvertiert werden". Stattdessen wird nur das ...
Formular verwendet, das Two zurückgibt und daher als false ausgewertet wird. Int ist kein Klassentyp.
...
, sondern das int C::*
, was ich noch nie gesehen hatte und nachschlagen musste. Hier finden Sie die Antwort darauf, was das ist und wofür es verwendet werden könnte: stackoverflow.com/questions/670734/…
Ich benutze gerne SFINAE
, um boolesche Bedingungen zu überprüfen.
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 */
}
Es kann sehr nützlich sein. Zum Beispiel habe ich damit überprüft, ob eine mit Operator Komma gesammelte Initialisiererliste nicht länger als eine feste Größe ist
template<int N>
struct Vector {
template<int M>
Vector(MyInitList<M> const& i, char(*)[M <= N] = 0) { /* ... */ }
}
Die Liste wird nur akzeptiert, wenn M kleiner als N ist, was bedeutet, dass die Initialisierungsliste nicht zu viele Elemente enthält.
Die Syntax char(*)[C]
bedeutet: Zeiger auf ein Array mit dem Elementtyp char und size C
. Wenn C
false (hier 0) ist, erhalten wir den ungültigen char(*)[0]
Zeiger auf ein Array mit der Größe Null: SFINAE macht es so, dass die Vorlage dann ignoriert wird.
Ausgedrückt mit boost::enable_if
, das sieht so aus
template<int N>
struct Vector {
template<int M>
Vector(MyInitList<M> const& i,
typename enable_if_c<(M <= N)>::type* = 0) { /* ... */ }
}
In der Praxis finde ich die Fähigkeit, Bedingungen zu überprüfen, oft eine nützliche Fähigkeit.
M <= N ? 1 : -1
könnte es vielleicht stattdessen funktionieren.
int foo[0]
. Ich bin nicht überrascht, dass es unterstützt wird, da es den sehr nützlichen Trick "Struktur endet mit einem Array mit 0 Längen" ermöglicht ( gcc.gnu.org/onlinedocs/gcc/Zero-Length.html ).
error C2466: cannot allocate an array of constant size 0
In C ++ 11 sind SFINAE-Tests viel schöner geworden. Hier sind einige Beispiele für häufige Verwendungen:
Wählen Sie abhängig von den Merkmalen eine Funktionsüberladung
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
}
Mit einem so genannten Typ-Sink-Idiom können Sie ziemlich willkürliche Tests für einen Typ durchführen, z. B. prüfen, ob er ein Mitglied hat und ob dieses Mitglied von einem bestimmten Typ ist
//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;
}
Hier ist ein Live-Beispiel: http://ideone.com/dHhyHE Ich habe kürzlich in meinem Blog einen ganzen Abschnitt über SFINAE und Tag-Versand geschrieben (schamloser Plug, aber relevant). Http://metaporky.blogspot.de/2014/08/ part-7-static-dispatch-function.html
Beachten Sie, dass es ab C ++ 14 ein std :: void_t gibt, das im Wesentlichen mit meinem TypeSink hier identisch ist.
TypeSinkT<decltype(std::declval<T&>().*(&T::bar))>
an einem Ort und dann TypeSinkT<decltype(&T::bar)>
an einem anderen verwenden? Auch ist das &
notwendig in std::declval<T&>
?
TypeSink
, C ++ 17 haben std::void_t
:)
Die enable_if- Bibliothek von Boost bietet eine schöne, saubere Oberfläche für die Verwendung von SFINAE. Eines meiner bevorzugten Anwendungsbeispiele befindet sich in der Boost.Iterator- Bibliothek. SFINAE wird verwendet, um Konvertierungen vom Iteratortyp zu aktivieren.
C ++ 17 bietet wahrscheinlich ein generisches Mittel zum Abfragen von Features. Siehe N4502 für Details, aber als eigenständiges Beispiel betrachten Sie Folgendes.
Dieser Teil ist der konstante Teil, setzen Sie ihn in eine Kopfzeile.
// 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 {};
Das folgende Beispiel aus N4502 zeigt die Verwendung:
// 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>;
Im Vergleich zu den anderen Implementierungen ist diese ziemlich einfach: Ein reduzierter Satz von Werkzeugen ( void_t
und detect
) reicht aus. Außerdem wurde berichtet (siehe N4502 ), dass es messbar effizienter ist (Kompilierungszeit und Compiler-Speicherverbrauch) als frühere Ansätze.
Hier ist ein Live-Beispiel , das Portabilitätsverbesserungen für GCC vor 5.1 enthält.
Hier ist ein weiteres (spätes) SFINAE- Beispiel, das auf Greg Rogers ' Antwort basiert :
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);
Auf diese Weise können Sie den value
Wert des 's überprüfen , um festzustellen, ob T
es sich um eine Klasse handelt oder nicht:
int main(void) {
std::cout << IsClassT<std::string>::value << std::endl; // true
std::cout << IsClassT<int>::value << std::endl; // false
return 0;
}
int C::*
in Ihrer Antwort? Wie kann C::*
ein Parametername sein?
int C::*
ist der Typ eines Zeigers auf eine int
Mitgliedsvariable von C
.
Hier ist ein guter Artikel von SFINAE: Eine Einführung in das SFINAE-Konzept von C ++: Introspektion eines Klassenmitglieds zur Kompilierungszeit .
Fassen Sie es wie folgt zusammen:
/*
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
ist ein Dienstprogramm, mit dem Sie einen "falschen Verweis" auf ein Objekt eines Typs erhalten, der nicht einfach zu erstellen ist. declval
ist sehr praktisch für unsere SFINAE-Konstruktionen.
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';
}
Hier verwende ich die Überladung von Vorlagenfunktionen (nicht direkt SFINAE), um zu bestimmen, ob ein Zeiger eine Funktion oder ein Elementklassenzeiger ist: ( Ist es möglich, die iostream cout / cerr-Elementfunktionszeiger zu korrigieren, die als 1 oder true gedruckt werden? )
#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;
}
Druckt
0. false
1. true
2. true
3. true
4. true
So wie der Code ist, könnte er (abhängig vom "guten" Willen des Compilers) einen Laufzeitaufruf für eine Funktion erzeugen, die true oder false zurückgibt. Wenn Sie die is_function_pointer(var)
Auswertung beim Kompilieren erzwingen möchten (zur Laufzeit werden keine Funktionsaufrufe ausgeführt), können Sie den constexpr
Variablentrick verwenden:
constexpr bool ispointer = is_function_pointer(var);
std::cout << "ispointer " << ispointer << std::endl;
Nach dem C ++ - Standard wird constexpr
garantiert, dass alle Variablen zur Kompilierungszeit ausgewertet werden ( Berechnen der Länge eines C-Strings zur Kompilierungszeit. Ist dies wirklich ein Nachteil? ).
Der folgende Code verwendet SFINAE, damit der Compiler eine Überladung basierend darauf auswählen kann, ob ein Typ eine bestimmte Methode hat oder nicht:
#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;
}
Ausgabe:
Float: 1 Int: -1