Szukaj…


enable_if

std::enable_if to wygodne narzędzie do używania warunków boolowskich do wyzwalania SFINAE. Jest zdefiniowany jako:

template <bool Cond, typename Result=void>
struct enable_if { };

template <typename Result>
struct enable_if<true, Result> {
    using type = Result;
};

Oznacza to, że enable_if<true, R>::type jest aliasem dla R , podczas gdy enable_if<false, T>::type jest źle sformułowany, ponieważ ta specjalizacja enable_if nie ma type członka typu.

std::enable_if może być użyte do ograniczenia szablonów:

int negate(int i) { return -i; }

template <class F>
auto negate(F f) { return -f(); }

W tym przypadku wywołanie negate(1) zakończy się niepowodzeniem z powodu niejednoznaczności. Ale drugie przeciążenie nie jest przeznaczone do stosowania dla typów integralnych, dlatego możemy dodać:

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

Teraz utworzenie wystąpienia negate<int> spowodowałoby niepowodzenie podstawienia, ponieważ !std::is_arithmetic<int>::value jest false . Ze względu na SFINAE nie jest to poważny błąd, ten kandydat jest po prostu usuwany z zestawu przeciążeń. W rezultacie negate(1) ma tylko jednego zdolnego do życia kandydata - który jest następnie wywoływany.

Kiedy z niego korzystać

Warto pamiętać, że std::enable_if jest pomocnikiem nad SFINAE, ale nie to sprawia, że SFINAE działa w pierwszej kolejności. Rozważmy te dwie alternatywy dla implementacji funkcjonalności podobnej do std::size , tj. size(arg) zestawu przeciążenia size(arg) który tworzy rozmiar kontenera lub tablicy:

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

Zakładając, że is_sizeable jest napisane odpowiednio, te dwie deklaracje powinny być dokładnie równoważne w odniesieniu do SFINAE. Który jest najłatwiejszy do napisania, a który najłatwiej przejrzeć i zrozumieć na pierwszy rzut oka?

Zastanówmy się teraz, w jaki sposób możemy zaimplementować pomocniki arytmetyczne, które unikną przepełnienia liczby całkowitej na korzyść zawijania lub zachowania modułowego. To znaczy, że np. incr(i, 3) będzie taki sam, jak i += 3 wyjątkiem faktu, że wynik zawsze będzie zdefiniowany, nawet jeśli i jest int z wartością INT_MAX . Są dwie możliwe alternatywy:

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

Po raz kolejny, który jest najłatwiejszy do napisania, a który najłatwiej przejrzeć i zrozumieć na pierwszy rzut oka?

Siłą std::enable_if jest to, jak gra się z refaktoryzacją i projektowaniem API. Jeśli wartość is_sizeable<Cont>::value ma odzwierciedlać, czy cont.size() jest poprawny, to samo użycie wyrażenia, jakie pojawia się dla size1 może być bardziej zwięzłe, chociaż może to zależeć od tego, czy is_sizeable będzie używany w kilku miejscach, czy nie . std::is_signed to ze std::is_signed co odzwierciedla jego zamiar znacznie wyraźniej niż wtedy, gdy jego implementacja incr1 do deklaracji incr1 .

void_t

C ++ 11

void_t to meta-funkcja, która odwzorowuje dowolny (liczbę) typów na typ void . Podstawowym celem void_t jest ułatwienie pisania cech typu.

std::void_t będzie częścią C ++ 17, ale do tego czasu implementacja jest niezwykle prosta:

template <class...> using void_t = void;

Niektóre kompilatory wymagają nieco innej implementacji:

template <class...>
struct make_void { using type = void; };

template <typename... T>
using void_t = typename make_void<T...>::type;

Podstawowym zastosowaniem void_t jest pisanie cech typu, które sprawdzają poprawność instrukcji. Na przykład sprawdźmy, czy typ ma funkcję foo() która nie przyjmuje żadnych argumentów:

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

Jak to działa? Kiedy próbuję utworzyć wartość has_foo<T>::value , spowoduje to, że kompilator spróbuje poszukać najlepszej specjalizacji dla has_foo<T, void> . Mamy dwie opcje: pierwotną i wtórną, która wymaga utworzenia wystąpienia tego wyrażenia:

  • Jeśli T ma funkcji składowej foo() , a następnie, niezależnie od typu, który powraca zostanie przekonwertowany do void , a specjalizacja korzystne jest podstawowym oparta na szablonie częściowych zasad zamawiania. Tak więc wartość has_foo<T>::value będzie true
  • Jeśli T nie ma takiej funkcji składowej (lub wymaga więcej niż jednego argumentu), to podstawienie kończy się niepowodzeniem dla specjalizacji i mamy tylko podstawowy szablon, na którym można wrócić. Dlatego wartość has_foo<T>::value ma has_foo<T>::value false .

Prostszy przypadek:

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

to nie używa std::declval ani decltype .

Możesz zauważyć wspólny wzorzec nieważnego argumentu. Możemy to rozdzielić:

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

który ukrywa użycie std::void_t i sprawia, że can_apply działa jak wskaźnik, czy typ can_apply jako pierwszy argument szablonu jest poprawnie uformowany po podstawieniu do niego innych typów. Poprzednie przykłady można teraz przepisać przy użyciu can_apply jako:

template<class T>
using ref_t = T&;

template<class T>
using can_reference = can_apply<ref_t, T>;    // Is T& well formed for T?

i:

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?

co wydaje się prostsze niż oryginalne wersje.

Istnieją propozycje post-C ++ 17 dla std cech podobnych do can_apply .

Walter Brown odkrył użyteczność void_t . W CppCon 2016 przedstawił wspaniałą prezentację na ten temat.

końcowy typ w szablonach funkcji

C ++ 11

Jedną z funkcji ograniczających jest użycie końcowego decltype do określenia typu zwracanego:

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

Jeśli convert_to_string() metodę convert_to_string() z argumentem, za pomocą którego mogę wywołać funkcję to_string() , to mam dwie realne funkcje dla details::convert_to_string() . Pierwszy jest preferowany, ponieważ konwersja z 0 na int jest lepszą niejawną sekwencją konwersji niż konwersja z 0 na ...

Jeśli convert_to_string() metodę convert_to_string() z argumentem, z którego nie mogę wywołać funkcji to_string() , wówczas tworzenie instancji szablonu pierwszej funkcji prowadzi do niepowodzenia podstawienia (nie ma decltype(to_string(val)) ). W rezultacie ten kandydat jest usuwany z zestawu przeciążeń. Drugi szablon funkcji jest nieograniczony, więc jest wybrany i zamiast tego przechodzimy przez operator<<(std::ostream&, T) . Jeśli ten jest niezdefiniowany, mamy twardy błąd kompilacji ze stosem szablonów w linii oss << val .

Co to jest SFINAE

SFINAE oznacza S ubstitution F ailure I S N Ot LP rror. Źle sformułowany kod wynikający z podstawienia typów (lub wartości) w celu utworzenia szablonu funkcji lub szablonu klasy nie jest trudnym błędem kompilacji, lecz jest traktowany jedynie jako błąd dedukcji.

Niepowodzenia w odliczaniu szablonów funkcji lub specjalizacji szablonów klas usuwają tego kandydata z zestawu rozważań - tak jakby początkowo ten nieudany kandydat nie istniał.

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. 

Tylko niepowodzenia podstawienia w bezpośrednim kontekście są uważane za niepowodzenia dedukcji, wszystkie pozostałe są uważane za poważne błędy.

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

Przykład motywacyjny


Jeśli na liście parametrów szablonu znajduje się paczka szablonów variadic, jak w poniższym fragmencie kodu:

template<typename ...Args> void func(Args &&...args) { //... };

Biblioteka standardowa (wcześniejsza niż C ++ 17) nie oferuje bezpośredniego sposobu zapisu enable_if w celu nałożenia ograniczeń SFINAE na wszystkie parametry w Args lub dowolny z parametrów w Args . C ++ 17 oferuje std::conjunction i std::disjunction które rozwiązują ten problem. Na przykład:

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

Jeśli nie masz dostępnego C ++ 17, istnieje kilka rozwiązań, aby je osiągnąć. Jednym z nich jest użycie klasy bazowej i częściowych specjalizacji , jak pokazano w odpowiedziach na to pytanie .

Alternatywnie można również ręcznie wprowadzić zachowanie std::conjunction i std::disjunction w dość prosty sposób. W poniższym przykładzie przedstawię implementacje i połączę je ze std::enable_if aby utworzyć dwa aliasy: enable_if_all i enable_if_any , które robią dokładnie to, co powinny semantycznie. Może to zapewnić bardziej skalowalne rozwiązanie.


Implementacja enable_if_all i enable_if_any


Najpierw seq_and std::conjunction i std::disjunction za pomocą odpowiednio dostosowanego seq_and i 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> {};  

Wówczas implementacja jest dość prosta:

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

W końcu niektórzy pomocnicy:

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;

Stosowanie


Użycie jest również proste:

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

jest wykryty

Aby uogólnić tworzenie cech typu: w oparciu o SFINAE istnieją cechy eksperymentalne detected_or lub detected_t , is_detected .

Z parametrami szablonu typename Default , template <typename...> Op i typename ... Args :

  • is_detected : alias of std::true_type lub std::false_type zależności od ważności Op<Args...>
  • detected_t : alias Op<Args...> lub nonesuch w zależności od ważności Op<Args...> .
  • detected_or : alias struct z value_t który is_detected i type , który jest Op<Args...> lub Default w zależności od ważności Op<Args...>

które można zaimplementować przy użyciu std::void_t dla SFINAE w następujący sposób:

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

Następnie można po prostu zaimplementować funkcje wykrywania obecności metody:

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

Rozdzielczość przeciążenia z dużą liczbą opcji

Jeśli musisz wybrać jedną z kilku opcji, włączenie tylko jednej za pomocą enable_if<> może być dość kłopotliwe, ponieważ kilka warunków również musi zostać zanegowanych.

Kolejność między przeciążeniami można zamiast tego wybrać za pomocą dziedziczenia, tj. Wysyłki tagu.

Zamiast testować to, co musi być dobrze uformowane, a także testować negację wszystkich innych wersji wersji, zamiast tego testujemy tylko to, czego potrzebujemy, najlepiej w decltype z końcowym powrotem.
Może to pozostawić kilka dobrze uformowanych opcji, rozróżniamy te, które używają „znaczników”, podobnie jak znaczniki iteratora-cechy ( random_access_tag i in.). Działa to, ponieważ bezpośrednie dopasowanie jest lepsze niż klasa podstawowa, co jest lepsze niż klasa podstawowa klasy podstawowej itp.

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

Istnieją inne metody powszechnie stosowane do rozróżnienia przeciążeń, takie jak dopasowanie ścisłe lepsze niż konwersja, lepsze niż elipsa.

Jednak wysyłanie tagów może obejmować dowolną liczbę opcji i jest nieco bardziej intencjonalne.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow