Sök…


enable_if

std::enable_if är ett bekvämt verktyg för att använda booleska förhållanden för att utlösa SFINAE. Det definieras som:

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

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

Det vill säga, enable_if<true, R>::type är ett alias för R , medan enable_if<false, T>::type är illa bildas som specialisering av enable_if inte har en type medlem typ.

std::enable_if kan användas för att begränsa mallar:

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

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

Här skulle ett samtal om att negate(1) misslyckas på grund av tvetydighet. Men den andra överbelastningen är inte avsedd att användas för integrerade typer, så vi kan lägga till:

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

Nu skulle omedelbar negate<int> resultera i ett substitutionsfel eftersom !std::is_arithmetic<int>::value är false . På grund av SFINAE är detta inte ett hårt fel, den här kandidaten tas helt enkelt bort från överbelastningsuppsättningen. Som ett resultat har negate(1) bara en enda livskraftig kandidat - som då kallas.

När du ska använda den

Det är värt att komma ihåg att std::enable_if är en hjälper ovanpå SFINAE, men det är inte det som gör att SFINAE fungerar i första hand. Låt oss överväga dessa två alternativ för att implementera funktionalitet som liknar std::size , dvs en överbelastningsuppsättning size(arg) som producerar storleken på en behållare eller array:

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

Om man antar att det är is_sizeable skrivet på lämpligt sätt bör dessa två förklaringar vara exakt likvärdiga med avseende på SFINAE. Vilket är det enklaste att skriva, och vilket är det enklaste att granska och förstå en överblick?

Låt oss nu överväga hur vi kanske vill implementera aritmetiska hjälpare som undviker signerat heltalöverskridande till förmån för lindning eller modulärt beteende. Vilket är att exempelvis incr(i, 3) skulle vara detsamma som i += 3 spara för att resultatet alltid skulle definieras även om i är ett int med värdet INT_MAX . Dessa är två möjliga alternativ:

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

Återigen vilken är lättast att skriva, och vilken är enklast att granska och förstå en överblick?

En styrka av std::enable_if är hur det spelar med refactoring och API-design. Om is_sizeable<Cont>::value är avsett att återspegla om cont.size() är giltigt, kan bara använda uttrycket som det verkar för size1 vara mer kortfattat, även om det kan bero på om is_sizeable skulle användas på flera platser eller inte . Kontrast det med std::is_signed vilket återspeglar dess avsikt mycket tydligare än när dess implementering läcker in i deklarationen om incr1 .

void_t

C ++ 11

void_t är en metafunktion som kartlägger alla (antal) typer för att skriva void . Det primära syftet med void_t är att underlätta skrivandet av typdrag.

std::void_t kommer att vara en del av C ++ 17, men fram till dess är det extremt enkelt att implementera:

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

Vissa kompilatorer kräver en något annan implementering:

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

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

Den primära tillämpningen av void_t är att skriva typdrag som kontrollerar giltigheten av ett uttalande. Låt oss till exempel kontrollera om en typ har en medlemsfunktion foo() som inte tar några argument:

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

Hur fungerar detta? När jag försöker instansera has_foo<T>::value kommer det att göra att kompilatorn försöker leta efter den bästa specialiseringen för has_foo<T, void> . Vi har två alternativ: det primära och det sekundära som innebär att det underliggande uttrycket måste instanseras:

  • Om T har en medlemsfunktion foo() , då oavsett vilken typ att avkastningen får omvandlas till void och specialisering är att föredra att den primära baserade på mallen partiella beställningsregler. Så has_foo<T>::value kommer att vara true
  • Om T inte har en sådan medlemsfunktion (eller den kräver mer än ett argument), misslyckas ersättningen för specialiseringen och vi har bara den primära mallen att falla tillbaka på. Därför är has_foo<T>::value false .

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

detta använder inte std::declval eller decltype .

Du kanske märker ett vanligt mönster för ett ogiltigt argument. Vi kan beräkna detta:

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

vilket döljer användningen av std::void_t och får can_apply fungera som en indikator om den typ som levereras som det första mallargumentet är välformat efter att de andra typerna har ersatts i det. De tidigare exemplen kan nu skrivas om med hjälp av can_apply som:

template<class T>
using ref_t = T&;

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

och:

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?

vilket verkar enklare än originalversionerna.

Det finns post-C ++ 17 förslag för std drag som liknar can_apply .

void_t av void_t upptäcktes av Walter Brown. Han gav en underbar presentation på CppCon 2016.

efterföljande decltype i funktionsmallar

C ++ 11

En av begränsningsfunktionerna är att använda släpande decltype att ange decltype :

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

Om jag kallar convert_to_string() med ett argument som jag kan åberopa to_string() , så har jag två genomförbara funktioner för details::convert_to_string() . Den första är att föredra eftersom omvandlingen från 0 till int är en bättre implicit omvandlingssekvens än omvandlingen från 0 till ...

Om jag kallar convert_to_string() med ett argument från vilket jag inte kan åberopa to_string() , leder den första funktionsmallinställningen till substitutionsfel (det finns ingen decltype(to_string(val)) ). Som ett resultat tas den kandidaten bort från överbelastningsuppsättningen. Den andra funktionsmallen är obegränsad, så den är vald och vi går istället igenom operator<<(std::ostream&, T) . Om den är odefinierad, har vi ett hårt sammanställningsfel med en mallstapel på linjen oss << val .

Vad är SFINAE

SFINAE står för S ubstitution F ailure I s N ot A n E rror. Illformad kod som resulterar från att ersätta typer (eller värden) för att instansera en funktionsmall eller en klassmall är inte ett hårt kompileringsfel, det behandlas bara som ett avdragsfel.

Avdragsfel vid instansierande funktionsmallar eller klassmallspecialiseringar tar bort den kandidaten från uppsättningen - som om den misslyckade kandidaten inte fanns till en början.

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. 

Endast substitutionsfel i det omedelbara sammanhanget betraktas som avdragsfel, alla andra betraktas som hårda fel.

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

Motiverande exempel


När du har ett variadiskt mallpaket i listan med mallparametrar, som i följande kodavsnitt:

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

Standardbiblioteket (före C ++ 17) erbjuder inget direkt sätt att skriva enable_if för att införa SFINAE-begränsningar på alla parametrarna i Args eller någon av parametrarna i Args . C ++ 17 erbjuder std::conjunction och std::disjunction som löser detta problem. Till exempel:

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

Om du inte har C ++ 17 tillgängliga finns det flera lösningar för att uppnå dessa. En av dem är att använda en bas-case-klass och delvisa specialiseringar , vilket visas i svar på denna fråga .

Alternativt kan man också för hand implementera beteendet std::conjunction och std::disjunction på ett ganska rakt fram. I följande exempel demonstrerar jag implementeringarna och kombinerar dem med std::enable_if att producera två alias: enable_if_all och enable_if_any , som gör exakt vad de ska semantiskt. Detta kan ge en mer skalbar lösning.


Implementering av enable_if_all och enable_if_any


Låt oss först emulera std::conjunction och std::disjunction med anpassade seq_and respektive 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> {};  

Då är implementeringen ganska rak:

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

Så småningom några hjälpare:

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;

Användande


Användningen är också rak:

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

is_detected

Att generalisera type_trait skapelse: baserat på SFINAE finns experimentella drag detected_or , detected_t , is_detected .

Med typename Default , template <typename...> Op och typename ... Args :

  • is_detected : alias of std::true_type eller std::false_type beroende på giltigheten för Op<Args...>
  • detected_t : alias för Op<Args...> eller nonesuch beroende på giltigheten för Op<Args...> .
  • detected_or value_t : alias för en struktur med value_t som is_detected , och type som är Op<Args...> eller Default beroende på giltigheten för Op<Args...>

som kan implementeras med std::void_t för SFINAE enligt följande:

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

Egenskaper för att upptäcka närvaro av metod kan sedan enkelt implementeras:

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

Överbelastningsupplösning med ett stort antal alternativ

Om du behöver välja mellan flera alternativ kan det vara ganska besvärligt att aktivera bara ett via enable_if<> eftersom flera villkor också måste negeras.

Beställningen mellan överbelastningar kan istället väljas med hjälp av arv, dvs.

Istället för att testa för det som måste vara välformat och även testa förnekandet av alla andra versioners villkor testar vi istället bara för vad vi behöver, helst i en decltype i en släpavkastning.
Detta kan lämna flera alternativ välformade, vi skiljer mellan de som använder "taggar", liknande iterator-drag-taggar ( random_access_tag et al). Detta fungerar eftersom en direkt matchning är bättre än en basklass, vilket är bättre att en basklass i en basklass, etc.

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

Det finns andra metoder som vanligtvis används för att skilja mellan överbelastningar, som att exakt matchning är bättre än konvertering, är bättre än ellips.

Emellertid kan tag-dispatch utvidgas till valfritt antal val och är lite tydligare i avsikt.



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow