C++
SFINAE (Ersättningsfel är inte något fel)
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
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 medlemsfunktionfoo()
, då oavsett vilken typ att avkastningen får omvandlas tillvoid
och specialisering är att föredra att den primära baserade på mallen partiella beställningsregler. Såhas_foo<T>::value
kommer att varatrue
- 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 ärhas_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
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
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 ofstd::true_type
ellerstd::false_type
beroende på giltigheten förOp<Args...>
-
detected_t
: alias förOp<Args...>
ellernonesuch
beroende på giltigheten förOp<Args...>
. -
detected_or
value_t
: alias för en struktur medvalue_t
somis_detected
, ochtype
som ärOp<Args...>
ellerDefault
beroende på giltigheten förOp<Args...>
som kan implementeras med std::void_t
för SFINAE enligt följande:
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.