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

C ++ 11

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 hat foo() , dann gleich welcher Art, die wird konvertiert zurück void , und die Spezialisierung auf die primäre auf der Grundlage der Vorlage Teilordnungsregeln bevorzugt. Also ist has_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 ist has_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

C ++ 11

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

C ++ 11

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 ​​von std::true_type oder std::false_type abhängig von der Gültigkeit von Op<Args...>
  • detected_t : Alias für Op<Args...> oder nonesuch in Abhängigkeit von ihrer Gültigkeit Op<Args...> .
  • detected_or : alias eine Struktur mit value_t welcher is_detected und type das ist Op<Args...> oder Default in Abhängigkeit von ihrer Gültigkeit Op<Args...>

std::void_t kann mit std::void_t für SFINAE wie folgt implementiert werden:

C ++ 17
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.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow