C++
SFINAE (Substitutionsfehler ist kein Fehler)
Suche…
enable_if
std::enable_if
ist ein praktisches Dienstprogramm, um boolesche Bedingungen zum Auslösen von SFINAE zu verwenden. Es ist definiert als:
template <bool Cond, typename Result=void>
struct enable_if { };
template <typename Result>
struct enable_if<true, Result> {
using type = Result;
};
Das heißt, enable_if<true, R>::type
ist ein Alias für R
, während enable_if<false, T>::type
wird als die Spezialisierung der schlecht gebildeten enable_if
keinen haben type
Elementtyp.
std::enable_if
können Sie Vorlagen einschränken:
int negate(int i) { return -i; }
template <class F>
auto negate(F f) { return -f(); }
Hier würde ein Aufruf zum negate(1)
aufgrund von Mehrdeutigkeit fehlschlagen. Die zweite Überladung soll jedoch nicht für ganzzahlige Typen verwendet werden. Daher können wir Folgendes hinzufügen:
int negate(int i) { return -i; }
template <class F, class = typename std::enable_if<!std::is_arithmetic<F>::value>::type>
auto negate(F f) { return -f(); }
Jetzt Instanziieren negate<int>
würde , da in einem Substitutionsfehler führen !std::is_arithmetic<int>::value
ist false
. Aufgrund von SFINAE ist dies kein schwerwiegender Fehler. Dieser Kandidat wird einfach aus dem Überlastungssatz entfernt. negate(1)
nur einen einzigen möglichen Kandidaten - der dann aufgerufen wird.
Wann verwenden?
Es ist erwähnenswert, dass std::enable_if
auf std::enable_if
ein Helfer ist, aber es ist nicht der Grund, warum SFINAE überhaupt erst funktioniert. Wir betrachten diese beiden Alternativen für die Implementierung einer Funktionalität, die der von std::size
ähnelt, dh einer Überladungssatzgröße size(arg)
, die die Größe eines Containers oder Arrays erzeugt:
// for containers
template<typename Cont>
auto size1(Cont const& cont) -> decltype( cont.size() );
// for arrays
template<typename Elt, std::size_t Size>
std::size_t size1(Elt const(&arr)[Size]);
// implementation omitted
template<typename Cont>
struct is_sizeable;
// for containers
template<typename Cont, std::enable_if_t<std::is_sizeable<Cont>::value, int> = 0>
auto size2(Cont const& cont);
// for arrays
template<typename Elt, std::size_t Size>
std::size_t size2(Elt const(&arr)[Size]);
Unter der Annahme, dass is_sizeable
entsprechend geschrieben ist, sollten diese beiden Deklarationen in Bezug auf SFINAE genau gleichwertig sein. Welches ist am einfachsten zu schreiben und welches ist am einfachsten auf einen Blick zu überprüfen und zu verstehen?
Lassen Sie uns nun überlegen, wie wir möglicherweise arithmetische Helfer implementieren möchten, die den Überlauf von signierten Integerzahlen zugunsten von Wrap-around oder modularem Verhalten vermeiden. Das heißt, dass zum Beispiel incr(i, 3)
das gleiche wie i += 3
wäre, INT_MAX
, dass das Ergebnis immer definiert würde, selbst wenn i
ein int
mit dem Wert INT_MAX
. Dies sind zwei mögliche Alternativen:
// handle signed types
template<typename Int>
auto incr1(Int& target, Int amount)
-> std::void_t<int[static_cast<Int>(-1) < static_cast<Int>(0)]>;
// handle unsigned types by just doing target += amount
// since unsigned arithmetic already behaves as intended
template<typename Int>
auto incr1(Int& target, Int amount)
-> std::void_t<int[static_cast<Int>(0) < static_cast<Int>(-1)]>;
template<typename Int, std::enable_if_t<std::is_signed<Int>::value, int> = 0>
void incr2(Int& target, Int amount);
template<typename Int, std::enable_if_t<std::is_unsigned<Int>::value, int> = 0>
void incr2(Int& target, Int amount);
Welches ist am einfachsten zu schreiben und welches ist am einfachsten auf einen Blick zu überprüfen und zu verstehen?
Eine Stärke von std::enable_if
ist, wie es mit Refactoring und API-Design spielt. Wenn is_sizeable<Cont>::value
bedeuten soll, ob cont.size()
gültig ist, kann die Verwendung des Ausdrucks, wie er für size1
erscheint, size1
sein, obwohl dies davon abhängen könnte, ob is_sizeable
an mehreren Stellen verwendet wird oder nicht . std::is_signed
das mit std::is_signed
was seine Absicht viel deutlicher widerspiegelt, als wenn die Implementierung in die Deklaration von incr1
.
void_t
void_t
ist eine Meta-Funktion, die beliebige (Anzahl) Typen dem Typ void
void_t
. Der Hauptzweck von void_t
ist es, das Schreiben von void_t
zu erleichtern.
std::void_t
wird Teil von C ++ 17 sein, aber bis dahin ist es äußerst einfach zu implementieren:
template <class...> using void_t = void;
Einige Compiler erfordern eine etwas andere Implementierung:
template <class...>
struct make_void { using type = void; };
template <typename... T>
using void_t = typename make_void<T...>::type;
Die Hauptanwendung von void_t
ist das Schreiben von void_t
, die die Gültigkeit einer Anweisung prüfen. Lassen Sie uns beispielsweise prüfen, ob ein Typ eine foo()
, die keine Argumente foo()
:
template <class T, class=void>
struct has_foo : std::false_type {};
template <class T>
struct has_foo<T, void_t<decltype(std::declval<T&>().foo())>> : std::true_type {};
Wie funktioniert das? Wenn ich versuche, has_foo<T>::value
zu instantiieren, wird der Compiler versuchen, nach der besten Spezialisierung für has_foo<T, void>
zu suchen. Wir haben zwei Optionen: die primäre und die sekundäre, bei der der zugrunde liegende Ausdruck instanziiert werden muss:
- Wenn
T
eine Member - Funktion hatfoo()
, dann gleich welcher Art, die wird konvertiert zurückvoid
, und die Spezialisierung auf die primäre auf der Grundlage der Vorlage Teilordnungsregeln bevorzugt. Also isthas_foo<T>::value
true
- Wenn
T
über keine solche Member-Funktion verfügt (oder mehr als ein Argument erforderlich ist), schlägt die Ersetzung für die Spezialisierung fehl und wir haben nur die primäre Vorlage, auf die zurückgegriffen werden kann. Daher isthas_foo<T>::value
false
.
Ein einfacherer Fall:
template<class T, class=void>
struct can_reference : std::false_type {};
template<class T>
struct can_reference<T, std::void_t<T&>> : std::true_type {};
dies verwendet nicht std::declval
oder decltype
.
Sie können ein allgemeines Muster eines ungültigen Arguments feststellen. Wir können das ausrechnen:
struct details {
template<template<class...>class Z, class=void, class...Ts>
struct can_apply:
std::false_type
{};
template<template<class...>class Z, class...Ts>
struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:
std::true_type
{};
};
template<template<class...>class Z, class...Ts>
using can_apply = details::can_apply<Z, void, Ts...>;
std::void_t
die Verwendung von std::void_t
und can_apply
als Indikator fungieren, der darauf can_apply
, ob der als erstes Vorlagenargument bereitgestellte Typ nach dem Ersetzen der anderen Typen can_apply
gebildet ist. Die vorherigen Beispiele können jetzt mit can_apply
neu can_apply
werden:
template<class T>
using ref_t = T&;
template<class T>
using can_reference = can_apply<ref_t, T>; // Is T& well formed for T?
und:
template<class T>
using dot_foo_r = decltype(std::declval<T&>().foo());
template<class T>
using can_dot_foo = can_apply< dot_foo_r, T >; // Is T.foo() well formed for T?
Das scheint einfacher als die Originalversionen.
Es gibt Post-C ++ 17 Vorschläge für std
Eigenschaften ähnlich wie can_apply
.
Der Nutzen von void_t
wurde von Walter Brown entdeckt. Er hat auf der CppCon 2016 eine wunderbare Präsentation darüber gegeben.
nachfolgender decltype in Funktionsvorlagen
Eine der einschränkenden Funktionen besteht darin, den Rückgabetyp mit einem decltype
anzugeben:
namespace details {
using std::to_string;
// this one is constrained on being able to call to_string(T)
template <class T>
auto convert_to_string(T const& val, int )
-> decltype(to_string(val))
{
return to_string(val);
}
// this one is unconstrained, but less preferred due to the ellipsis argument
template <class T>
std::string convert_to_string(T const& val, ... )
{
std::ostringstream oss;
oss << val;
return oss.str();
}
}
template <class T>
std::string convert_to_string(T const& val)
{
return details::convert_to_string(val, 0);
}
Wenn ich convert_to_string()
mit einem Argument aufrufen, mit dem ich to_string()
aufrufen kann, to_string()
ich zwei details::convert_to_string()
Funktionen für details::convert_to_string()
. Die erste ist bevorzugt, da die Umwandlung von 0
in int
eine bessere implizite Umwandlungssequenz ist als die Umwandlung von 0
in ...
Wenn ich convert_to_string()
mit einem Argument aufrufen, aus dem ich to_string()
nicht aufrufen to_string()
, führt die Instanziierung der ersten Funktionsschablone zum Substitutionsfehler (es gibt keinen decltype(to_string(val))
). Dieser Kandidat wird daher aus dem Überlastungssatz entfernt. Die zweite Funktionsvorlage ist nicht eingeschränkt, also wird sie ausgewählt und wir gehen stattdessen durch den operator<<(std::ostream&, T)
. Wenn dieser nicht definiert ist, liegt ein schwerwiegender Kompilierungsfehler mit einem Vorlagenstapel in der Zeile oss << val
.
Was ist SFINAE?
SFINAE steht für S ubstitution F ailure I s N A n E rror ot. Ungeformter Code, der sich aus dem Ersetzen von Typen (oder Werten) ergibt, um eine Funktionsvorlage oder eine Klassenvorlage zu instanziieren, ist kein schwerwiegender Kompilierungsfehler, sondern wird nur als Deduktionsfehler behandelt.
Abzugsfehler bei der Instanziierung von Funktionsvorlagen oder Klassenvorlagen-Spezialisierungen entfernen diesen Kandidaten aus der Menge der Gegenleistung - als ob der fehlerhafte Kandidat nicht vorhanden wäre.
template <class T>
auto begin(T& c) -> decltype(c.begin()) { return c.begin(); }
template <class T, size_t N>
T* begin(T (&arr)[N]) { return arr; }
int vals[10];
begin(vals); // OK. The first function template substitution fails because
// vals.begin() is ill-formed. This is not an error! That function
// is just removed from consideration as a viable overload candidate,
// leaving us with the array overload.
Nur Substitutionsfehler im unmittelbaren Kontext werden als Abzugsfehler betrachtet, alle anderen werden als harte Fehler betrachtet.
template <class T>
void add_one(T& val) { val += 1; }
int i = 4;
add_one(i); // ok
std::string msg = "Hello";
add_one(msg); // error. msg += 1 is ill-formed for std::string, but this
// failure is NOT in the immediate context of substituting T
enable_if_all / enable_if_any
Motivationsbeispiel
Wenn Sie ein variadisches Vorlagenpaket in der Vorlagenparameterliste haben, wie im folgenden Codeausschnitt:
template<typename ...Args> void func(Args &&...args) { //... };
Die Standardbibliothek (vor C ++ 17) bietet keine direkte Möglichkeit, enable_if zu schreiben, um SFINAE-Einschränkungen für alle Parameter in Args
oder für alle Parameter in Args
. C ++ 17 bietet std::conjunction
und std::disjunction
die dieses Problem lösen. Zum Beispiel:
/// C++17: SFINAE constraints on all of the parameters in Args.
template<typename ...Args,
std::enable_if_t<std::conjunction_v<custom_conditions_v<Args>...>>* = nullptr>
void func(Args &&...args) { //... };
/// C++17: SFINAE constraints on any of the parameters in Args.
template<typename ...Args,
std::enable_if_t<std::disjunction_v<custom_conditions_v<Args>...>>* = nullptr>
void func(Args &&...args) { //... };
Wenn Sie nicht über C ++ 17 verfügen, gibt es mehrere Lösungen, um dies zu erreichen. Eine davon besteht darin, eine Basisfallklasse und Teilspezialisierungen zu verwenden , wie die Antworten auf diese Frage zeigen .
Alternativ kann man auch das Verhalten von std::conjunction
und std::disjunction
auf recht einfache Weise implementieren. Im folgenden Beispiel werde ich die Implementierungen demonstrieren und sie mit std::enable_if
, um zwei Aliasnamen zu erzeugen: enable_if_all
und enable_if_any
, die genau das tun, was sie semantisch tun sollen. Dies kann eine skalierbarere Lösung bieten.
Implementierung von enable_if_all
und enable_if_any
Zuerst emulieren Sie std::conjunction
und std::disjunction
Verwendung von benutzerdefiniertem seq_and
bzw. seq_or
:
/// Helper for prior to C++14.
template<bool B, class T, class F >
using conditional_t = typename std::conditional<B,T,F>::type;
/// Emulate C++17 std::conjunction.
template<bool...> struct seq_or: std::false_type {};
template<bool...> struct seq_and: std::true_type {};
template<bool B1, bool... Bs>
struct seq_or<B1,Bs...>:
conditional_t<B1,std::true_type,seq_or<Bs...>> {};
template<bool B1, bool... Bs>
struct seq_and<B1,Bs...>:
conditional_t<B1,seq_and<Bs...>,std::false_type> {};
Dann ist die Implementierung recht unkompliziert:
template<bool... Bs>
using enable_if_any = std::enable_if<seq_or<Bs...>::value>;
template<bool... Bs>
using enable_if_all = std::enable_if<seq_and<Bs...>::value>;
Eventuell einige Helfer:
template<bool... Bs>
using enable_if_any_t = typename enable_if_any<Bs...>::type;
template<bool... Bs>
using enable_if_all_t = typename enable_if_all<Bs...>::type;
Verwendungszweck
Die Verwendung ist auch unkompliziert:
/// SFINAE constraints on all of the parameters in Args.
template<typename ...Args,
enable_if_all_t<custom_conditions_v<Args>...>* = nullptr>
void func(Args &&...args) { //... };
/// SFINAE constraints on any of the parameters in Args.
template<typename ...Args,
enable_if_any_t<custom_conditions_v<Args>...>* = nullptr>
void func(Args &&...args) { //... };
ist angeschlossen
Um die Typ_trait-Erzeugung zu verallgemeinern: Basierend auf SFINAE gibt es experimentelle Merkmale, die detected_or
, detected_t
und is_detected
.
Mit Template-Parametern typename Default
, template <typename...> Op
und typename ... Args
:
-
is_detected
: Alias vonstd::true_type
oderstd::false_type
abhängig von der Gültigkeit vonOp<Args...>
-
detected_t
: Alias fürOp<Args...>
odernonesuch
in Abhängigkeit von ihrer GültigkeitOp<Args...>
. -
detected_or
: alias eine Struktur mitvalue_t
welcheris_detected
undtype
das istOp<Args...>
oderDefault
in Abhängigkeit von ihrer GültigkeitOp<Args...>
std::void_t
kann mit std::void_t
für SFINAE wie folgt implementiert werden:
namespace detail {
template <class Default, class AlwaysVoid,
template<class...> class Op, class... Args>
struct detector
{
using value_t = std::false_type;
using type = Default;
};
template <class Default, template<class...> class Op, class... Args>
struct detector<Default, std::void_t<Op<Args...>>, Op, Args...>
{
using value_t = std::true_type;
using type = Op<Args...>;
};
} // namespace detail
// special type to indicate detection failure
struct nonesuch {
nonesuch() = delete;
~nonesuch() = delete;
nonesuch(nonesuch const&) = delete;
void operator=(nonesuch const&) = delete;
};
template <template<class...> class Op, class... Args>
using is_detected =
typename detail::detector<nonesuch, void, Op, Args...>::value_t;
template <template<class...> class Op, class... Args>
using detected_t = typename detail::detector<nonesuch, void, Op, Args...>::type;
template <class Default, template<class...> class Op, class... Args>
using detected_or = detail::detector<Default, void, Op, Args...>;
Eigenschaften zum Erkennen des Vorhandenseins einer Methode können dann einfach implementiert werden:
typename <typename T, typename ...Ts>
using foo_type = decltype(std::declval<T>().foo(std::declval<Ts>()...));
struct C1 {};
struct C2 {
int foo(char) const;
};
template <typename T>
using has_foo_char = is_detected<foo_type, T, char>;
static_assert(!has_foo_char<C1>::value, "Unexpected");
static_assert(has_foo_char<C2>::value, "Unexpected");
static_assert(std::is_same<int, detected_t<foo_type, C2, char>>::value,
"Unexpected");
static_assert(std::is_same<void, // Default
detected_or<void, foo_type, C1, char>>::value,
"Unexpected");
static_assert(std::is_same<int, detected_or<void, foo_type, C2, char>>::value,
"Unexpected");
Überlastauflösung mit vielen Optionen
Wenn Sie zwischen mehreren Optionen auswählen müssen, kann die Aktivierung nur einer über enable_if<>
ziemlich umständlich sein, da mehrere Bedingungen ebenfalls negiert werden müssen.
Die Reihenfolge zwischen Überladungen kann stattdessen über Vererbung, dh Tag-Versand, ausgewählt werden.
Anstatt zu testen, was gut decltype
, und auch die Negation aller anderen Versionsbedingungen zu testen, testen wir stattdessen genau das, was wir brauchen, vorzugsweise in einem decltype
in einer nachlaufenden Rückgabe.
Dies kann mehrere Optionen random_access_tag
, wir unterscheiden zwischen denjenigen, die "Tags" verwenden, ähnlich wie bei Iterator-Trait-Tags ( random_access_tag
et al). Dies funktioniert, weil eine direkte Übereinstimmung besser ist als eine Basisklasse. Dies ist besser als eine Basisklasse einer Basisklasse usw.
#include <algorithm>
#include <iterator>
namespace detail
{
// this gives us infinite types, that inherit from each other
template<std::size_t N>
struct pick : pick<N-1> {};
template<>
struct pick<0> {};
// the overload we want to be preferred have a higher N in pick<N>
// this is the first helper template function
template<typename T>
auto stable_sort(T& t, pick<2>)
-> decltype( t.stable_sort(), void() )
{
// if the container have a member stable_sort, use that
t.stable_sort();
}
// this helper will be second best match
template<typename T>
auto stable_sort(T& t, pick<1>)
-> decltype( t.sort(), void() )
{
// if the container have a member sort, but no member stable_sort
// it's customary that the sort member is stable
t.sort();
}
// this helper will be picked last
template<typename T>
auto stable_sort(T& t, pick<0>)
-> decltype( std::stable_sort(std::begin(t), std::end(t)), void() )
{
// the container have neither a member sort, nor member stable_sort
std::stable_sort(std::begin(t), std::end(t));
}
}
// this is the function the user calls. it will dispatch the call
// to the correct implementation with the help of 'tags'.
template<typename T>
void stable_sort(T& t)
{
// use an N that is higher that any used above.
// this will pick the highest overload that is well formed.
detail::stable_sort(t, detail::pick<10>{});
}
Es gibt andere Methoden, die üblicherweise zur Unterscheidung zwischen Überladungen verwendet werden, z. B. dass exakte Übereinstimmungen besser sind als die Umwandlung und besser als Ellipsen.
Der Tag-Versand kann sich jedoch auf eine beliebige Anzahl von Optionen erstrecken und ist in der Absicht etwas klarer.