Zoeken…


enable_if

std::enable_if is een handig hulpprogramma om booleaanse voorwaarden te gebruiken om SFINAE te activeren. Het is gedefinieerd als:

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

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

Dat wil zeggen, enable_if<true, R>::type is een alias voor R , terwijl enable_if<false, T>::type slecht gevormd is omdat die specialisatie van enable_if geen type enable_if heeft.

std::enable_if kan worden gebruikt om sjablonen te beperken:

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

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

Hier zou een oproep tot negate(1) mislukken vanwege dubbelzinnigheid. Maar de tweede overbelasting is niet bedoeld om te worden gebruikt voor integrale typen, dus we kunnen toevoegen:

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, het negate<int> zou resulteren in een substitutiefout omdat !std::is_arithmetic<int>::value false . Vanwege SFINAE is dit geen harde fout, deze kandidaat wordt eenvoudig uit de overbelastingsset verwijderd. Als gevolg hiervan heeft negate(1) slechts één enkele levensvatbare kandidaat - die vervolgens wordt genoemd.

Wanneer gebruiken

Het is de moeite waard om in gedachten te houden dat std::enable_if een hulp is bovenop SFINAE, maar het is niet wat SFINAE in de eerste plaats laat werken. Laten we deze twee alternatieven overwegen voor het implementeren van functionaliteit die vergelijkbaar is met std::size , dat wil zeggen een overload set size(arg) die de grootte van een container of array produceert:

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

Ervan uitgaande dat is is_sizeable is correct geschreven, moeten deze twee verklaringen precies gelijk zijn met betrekking tot SFINAE. Wat is het gemakkelijkst om te schrijven en wat is het gemakkelijkst om in één oogopslag te beoordelen en te begrijpen?

Laten we nu eens kijken hoe we rekenkundige helpers willen implementeren die een ondertekende overloop van integers vermijden ten gunste van wrap-around of modulair gedrag. Dat wil zeggen dat bijvoorbeeld incr(i, 3) hetzelfde zou zijn als i += 3 behalve dat het resultaat altijd zou worden gedefinieerd, zelfs als i een int met waarde INT_MAX . Dit zijn twee mogelijke alternatieven:

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

Nogmaals, wat is het gemakkelijkst om te schrijven, en wat is het gemakkelijkst te beoordelen en in één oogopslag te begrijpen?

Een sterkte van std::enable_if is hoe het speelt met refactoring en API-ontwerp. Als is_sizeable<Cont>::value bedoeld is om aan te geven of cont.size() geldig is, dan kan alleen het gebruik van de uitdrukking zoals die voor size1 verschijnt beknopter zijn, hoewel dat kan afhangen van of is_sizeable op verschillende plaatsen zou worden gebruikt of niet . std::is_signed dat met std::is_signed dat zijn intentie veel duidelijker weergeeft dan wanneer de implementatie ervan lekt in de declaratie van incr1 .

void_t

C ++ 11

void_t is een void_t die elk (aantal) typen void_t aan void . Het primaire doel van void_t is het schrijven van void_t te vergemakkelijken.

std::void_t zal onderdeel zijn van C ++ 17, maar tot die tijd is het uiterst eenvoudig om te implementeren:

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

Sommige compilers vereisen een iets andere implementatie:

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

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

De primaire toepassing van void_t is het schrijven van void_t die de geldigheid van een verklaring controleren. Laten we bijvoorbeeld controleren of een type een foo() waarvoor geen argumenten nodig zijn:

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

Hoe werkt dit? Wanneer ik has_foo<T>::value probeer te instantiëren, zal de compiler proberen te zoeken naar de beste specialisatie voor has_foo<T, void> . We hebben twee opties: de primaire en deze secundaire waarbij de onderliggende expressie moet worden geïnstantieerd:

  • Als T heeft wel een lid functie foo() , dan is ongeacht het type dat het rendement wordt omgezet in void , en de specialisatie heeft de voorkeur om de primaire basis van de sjabloon te bestellen regels gedeeltelijke. Dus has_foo<T>::value zal true
  • Als T niet zo'n lidfunctie heeft (of het vereist meer dan één argument), dan mislukt vervanging voor de specialisatie en hebben we alleen het primaire sjabloon om op terug te vallen. Vandaar dat has_foo<T>::value false .

Een eenvoudiger geval:

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

dit maakt geen gebruik van std::declval of decltype .

U kunt een gemeenschappelijk patroon van een ongeldig argument opmerken. We kunnen dit uitrekenen:

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

die het gebruik van std::void_t en ervoor zorgt dat can_apply fungeren als een indicator of het type dat als het eerste sjabloonargument is can_apply , goed is gevormd nadat de andere typen erin zijn vervangen. De vorige voorbeelden kunnen nu worden herschreven met can_apply als:

template<class T>
using ref_t = T&;

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

en:

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?

wat eenvoudiger lijkt dan de originele versies.

Er zijn post-C ++ 17 voorstellen voor std eigenschappen vergelijkbaar met can_apply .

Het nut van void_t werd ontdekt door Walter Brown. Hij gaf er een prachtige presentatie over op CppCon 2016.

volgde DEC-type in functiesjablonen

C ++ 11

Een van de beperkende functies is om het decltype te gebruiken om het decltype te geven:

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

Als ik convert_to_string() aanroep met een argument waarmee ik to_string() kan aanroepen, heb ik twee uitvoerbare functies voor details::convert_to_string() . De eerste heeft de voorkeur omdat de conversie van 0 naar int een betere impliciete conversie is dan de conversie van 0 naar ...

Als ik convert_to_string() aanroep met een argument van waaruit ik to_string() niet kan aanroepen, dan leidt de instantiatie van de eerste functiesjabloon tot substitutiefout (er is geen decltype(to_string(val)) ). Als gevolg hiervan wordt die kandidaat uit de overbelastingsset verwijderd. Het tweede functiesjabloon is onbeperkt, dus het is geselecteerd en we gaan in plaats daarvan door operator<<(std::ostream&, T) . Als die niet is gedefinieerd, hebben we een harde compileerfout met een sjabloonstapel op de regel oss << val .

Wat is SFINAE

SFINAE staat voor S ubstitution F ailure I s N ot A n E rror. Onjuist gevormde code die het gevolg is van het vervangen van typen (of waarden) om een functiesjabloon of klassensjabloon te instantiëren, is geen harde compileerfout, het wordt alleen behandeld als een aftrekfout.

Aftrekfouten bij instantiërende functiesjablonen of klassensjabloonspecialisaties verwijderen die kandidaat uit de reeks van overweging - alsof die mislukte kandidaat in eerste instantie niet bestond.

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. 

Alleen substitutiefouten in de onmiddellijke context worden beschouwd als aftrekfouten, alle andere worden beschouwd als harde fouten.

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

Motiverend voorbeeld


Wanneer u een variadisch sjabloonpakket in de lijst met sjabloonparameters hebt, zoals in het volgende codefragment:

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

De standaardbibliotheek (vóór C ++ 17) biedt geen directe manier om enable_if te schrijven om SFINAE-beperkingen op te leggen aan alle parameters in Args of een van de parameters in Args . C ++ 17 biedt std::conjunction en std::disjunction waarmee dit probleem wordt opgelost. Bijvoorbeeld:

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

Als u C ++ 17 niet beschikbaar hebt, zijn er verschillende oplossingen om dit te bereiken. Een daarvan is het gebruik van een base-case-klasse en gedeeltelijke specialisaties , zoals aangetoond in de antwoorden op deze vraag .

Als alternatief kan men ook het gedrag van std::conjunction en std::disjunction op een vrij eenvoudige manier met de hand implementeren. In het volgende voorbeeld zal ik de implementaties demonstreren en combineren met std::enable_if om twee alias te produceren: enable_if_all en enable_if_any , die precies doen wat ze verondersteld worden semantisch te doen. Dit kan een meer schaalbare oplossing bieden.


Implementatie van enable_if_all en enable_if_any


Laten we eerst std::conjunction en std::disjunction seq_or respectievelijk aangepaste seq_and en 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> {};  

Dan is de implementatie vrij eenvoudig:

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

Uiteindelijk enkele helpers:

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;

Gebruik


Het gebruik is ook eenvoudig:

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

Om type_trait creatie generaliseren: gebaseerd op SFINAE er experimentele eigenschappen detected_or , detected_t , is_detected .

Met sjabloonparameters typename Default , template <typename...> Op en typename ... Args :

  • is_detected : alias van std::true_type of std::false_type afhankelijk van de geldigheid van Op<Args...>
  • detected_t : alias van Op<Args...> of nonesuch afhankelijk van de geldigheid van Op<Args...> .
  • detected_or : alias van een structuur met value_t die is_detected , en type dat is Op<Args...> of Default , afhankelijk van de geldigheid van Op<Args...>

die kan worden geïmplementeerd met std::void_t voor SFINAE als volgt:

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

Kenmerken om de aanwezigheid van de methode te detecteren, kunnen vervolgens eenvoudig worden geïmplementeerd:

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

Overbelastingsresolutie met een groot aantal opties

Als u tussen verschillende opties moet kiezen, kan het inschakelen van slechts één via enable_if<> behoorlijk omslachtig zijn, omdat verschillende voorwaarden ook moeten worden genegeerd.

De volgorde tussen overbelastingen kan in plaats daarvan worden geselecteerd met behulp van overerving, dwz tagverzending.

In plaats van te testen op het ding dat goed moet zijn gevormd, en ook het testen van de ontkenning van alle voorwaarden van de andere versies, testen we in plaats daarvan alleen wat we nodig hebben, bij voorkeur in een decltype in een achterstand.
Dit kan verschillende opties goed gevormd laten, we maken een onderscheid tussen die met 'tags', vergelijkbaar met iterator-trait tags ( random_access_tag et al). Dit werkt omdat een directe match beter is dan een basisklasse, wat beter is dan een basisklasse van een basisklasse, enz.

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

Er zijn andere methoden die gewoonlijk worden gebruikt om onderscheid te maken tussen overbelastingen, zoals exact zoeken beter is dan conversie, beter dan ellips.

Tag-verzending kan zich echter uitstrekken tot een willekeurig aantal keuzes en is wat duidelijker van opzet.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow