C++
SFINAE (échec de substitution n'est pas une erreur)
Recherche…
enable_if
std::enable_if
est un utilitaire pratique pour utiliser des conditions booléennes pour déclencher SFINAE. Il est défini comme suit:
template <bool Cond, typename Result=void>
struct enable_if { };
template <typename Result>
struct enable_if<true, Result> {
using type = Result;
};
Autrement dit, enable_if<true, R>::type
est un alias pour R
, alors que enable_if<false, T>::type
est mal formé car cette spécialisation de enable_if
n'a pas de type
membre.
std::enable_if
peut être utilisé pour contraindre les templates:
int negate(int i) { return -i; }
template <class F>
auto negate(F f) { return -f(); }
Ici, un appel à negate(1)
échouerait à cause de l'ambiguïté. Mais la deuxième surcharge n'est pas destinée à être utilisée pour les types intégraux, nous pouvons donc ajouter:
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(); }
Maintenant, l'instanciation de la negate<int>
entraînerait un échec de substitution car !std::is_arithmetic<int>::value
est false
. En raison de SFINAE, il ne s’agit pas d’une erreur grave, ce candidat est simplement supprimé de l’ensemble de surcharge. En conséquence, negate(1)
n'a qu'un seul candidat viable - qui est alors appelé.
Quand l'utiliser
Il convient de garder à l'esprit que std::enable_if
est une aide en plus de SFINAE, mais ce n'est pas ce qui fait que SFINAE fonctionne en premier lieu. Considérons ces deux alternatives pour implémenter des fonctionnalités similaires à std::size
, à savoir une size(arg)
jeu de surcharge size(arg)
qui produit la taille d'un conteneur ou d'un tableau:
// 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]);
En supposant que is_sizeable
est écrit correctement, ces deux déclarations doivent être exactement équivalentes à SFINAE. Quel est le plus facile à écrire et quel est le plus facile à revoir et à comprendre en un coup d’œil?
Considérons maintenant comment nous pourrions implémenter des aides arithmétiques qui évitent le débordement d'entier signé en faveur d'un comportement modulaire ou de bouclage. Ce qui veut dire que, par exemple, incr(i, 3)
serait le même que i += 3
sauf que le résultat serait toujours défini même si i
est un int
avec la valeur INT_MAX
. Ce sont deux alternatives possibles:
// 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);
Encore une fois, quel est le plus facile à écrire et quel est le plus facile à revoir et à comprendre en un coup d’œil?
La force de std::enable_if
réside dans la manière dont il joue avec le refactoring et la conception des API. Si is_sizeable<Cont>::value
est censé cont.size()
si cont.size()
est valide, alors il suffit d'utiliser l'expression telle qu'elle apparaît pour size1
, bien que cela dépende du fait que is_sizeable
soit utilisé à plusieurs endroits ou non. . Contraste avec std::is_signed
qui reflète son intention beaucoup plus clairement que lorsque son implémentation fuit dans la déclaration de incr1
.
void_t
void_t
est une méta-fonction qui mappe tout (nombre de) types à taper void
. Le but premier de void_t
est de faciliter l'écriture des caractères de type.
std::void_t
fera partie de C ++ 17, mais d'ici là, il est extrêmement simple à implémenter:
template <class...> using void_t = void;
Certains compilateurs nécessitent une implémentation légèrement différente:
template <class...>
struct make_void { using type = void; };
template <typename... T>
using void_t = typename make_void<T...>::type;
L'application principale de void_t
consiste à écrire des traits de type qui vérifient la validité d'une instruction. Par exemple, vérifions si un type a une fonction membre foo()
qui ne prend aucun 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 {};
Comment cela marche-t-il? Lorsque j'essaye d'instancier has_foo<T>::value
, le compilateur essaiera de rechercher la meilleure spécialisation pour has_foo<T, void>
. Nous avons deux options: la primaire et cette secondaire qui consiste à instancier cette expression sous-jacente:
- Si
T
a une fonction membrefoo()
, alors quel que soit le type qui retourne est converti envoid
, et la spécialisation est préférable au primaire sur la base des règles de classement partiel de modèle. Donchas_foo<T>::value
seratrue
- Si
T
n'a pas une telle fonction membre (ou nécessite plus d'un argument), la substitution échoue pour la spécialisation et nous n'avons que le modèle principal à utiliser. Par conséquent,has_foo<T>::value
estfalse
.
Un cas plus simple:
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 {};
cela n'utilise pas std::declval
ou decltype
.
Vous remarquerez peut-être un schéma commun d'un argument vide. Nous pouvons prendre ceci en compte:
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...>;
qui cache l'utilisation de std::void_t
et fait que can_apply
agit comme un indicateur can_apply
si le type fourni en tant que premier argument du template est bien formé après y avoir substitué les autres types. Les exemples précédents peuvent maintenant être réécrits en utilisant can_apply
comme:
template<class T>
using ref_t = T&;
template<class T>
using can_reference = can_apply<ref_t, T>; // Is T& well formed for T?
et:
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?
ce qui semble plus simple que les versions originales.
Il existe des propositions post-C ++ 17 pour les traits std
similaires à can_apply
.
L'utilité de void_t
été découverte par Walter Brown. Il a fait une magnifique présentation à CppCon 2016.
le decltype de fin dans les modèles de fonction
L'une des fonctions contraignantes consiste à utiliser decltype
fin pour spécifier le type de retour:
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);
}
Si j'appelle convert_to_string()
avec un argument avec lequel je peux invoquer to_string()
, j'ai deux fonctions viables pour les details::convert_to_string()
. Le premier est préférable car la conversion de 0
en int
est une meilleure séquence de conversion implicite que la conversion de 0
en ...
Si j'appelle convert_to_string()
avec un argument à partir duquel je ne peux pas appeler to_string()
, la première instanciation de modèle de fonction entraîne un échec de substitution (il n'y a pas de decltype(to_string(val))
). Par conséquent, ce candidat est supprimé de l'ensemble de surcharge. Le second modèle de fonction n'est pas contraint, il est donc sélectionné et nous passons par l' operator<<(std::ostream&, T)
. Si celui-ci n'est pas défini, nous avons une erreur de compilation avec une pile de modèles sur la ligne oss << val
.
Qu'est-ce que SFINAE?
SFINAE représente S ubstitution D tout manquement I S OT N A n E rror. Un code mal formé résultant de la substitution de types (ou de valeurs) pour instancier un modèle de fonction ou un modèle de classe n'est pas une erreur de compilation difficile, il est uniquement traité comme un échec de déduction.
Les échecs de déduction sur les modèles de fonction d'instanciation ou les spécialisations de modèle de classe suppriment ce candidat de l'ensemble considéré - comme si ce candidat en échec n'existait pas au départ.
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.
Seuls les échecs de substitution dans le contexte immédiat sont considérés comme des échecs de déduction, tous les autres sont considérés comme des erreurs graves.
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
Exemple de motivation
Lorsque vous avez un pack de modèles variadic dans la liste des paramètres du modèle, comme dans l'extrait de code suivant:
template<typename ...Args> void func(Args &&...args) { //... };
La bibliothèque standard (antérieure à C ++ 17) n'offre aucun moyen direct d'écrire enable_if pour imposer des contraintes SFINAE à tous les paramètres d' Args
ou à l' un des paramètres d' Args
. C ++ 17 offre std::conjunction
et std::disjunction
qui résolvent ce problème. Par exemple:
/// 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) { //... };
Si vous ne disposez pas de C ++ 17, il existe plusieurs solutions pour y parvenir. L'une d'entre elles consiste à utiliser une classe de base et des spécialisations partielles , comme le montrent les réponses à cette question .
Alternativement, on peut aussi implémenter le comportement de std::conjunction
et de std::disjunction
de manière assez directe. Dans l'exemple suivant, je vais présenter les implémentations et les combiner avec std::enable_if
pour produire deux alias: enable_if_all
et enable_if_any
, qui font exactement ce qu'ils sont supposés sémantiquement. Cela peut fournir une solution plus évolutive.
Implémentation de enable_if_all
et enable_if_any
Commençons par émuler std::conjunction
et std::disjunction
utilisant respectivement seq_and
et 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> {};
Ensuite, la mise en œuvre est assez simple:
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>;
Finalement, quelques assistants:
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;
Usage
L'utilisation est également simple:
/// 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) { //... };
est détecté
Pour généraliser la création type_trait: basée sur SFINAE il y a des traits expérimentaux detected_or
, detected_t
, is_detected
.
Avec les paramètres du template typename Default
, template <typename...> Op
et typename ... Args
:
-
is_detected
: alias destd::true_type
oustd::false_type
fonction de la validité deOp<Args...>
-
detected_t
: aliasOp<Args...>
ounonesuch
selon la validité deOp<Args...>
. -
detected_or
: alias d'une struct avecvalue_t
qui estis_detected
, et letype
qui estOp<Args...>
ouDefault
en fonction de validitéOp<Args...>
qui peut être implémenté en utilisant std::void_t
pour SFINAE comme suit:
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...>;
Les caractéristiques permettant de détecter la présence de la méthode peuvent alors être simplement implémentées:
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");
Résolution de surcharge avec un grand nombre d'options
Si vous avez besoin de choisir entre plusieurs options, l'activation d'une seule via enable_if<>
peut être assez compliquée, car plusieurs conditions doivent également être annulées.
L'ordre entre les surcharges peut être sélectionné à l'aide de l'héritage, c.-à-d.
Au lieu de tester la chose qui doit être bien formée et de tester la négation de toutes les autres conditions de version, nous testons plutôt ce dont nous avons besoin, de préférence dans un decltype
de retour dans un retour.
Cela peut laisser plusieurs options bien formées, nous distinguons celles utilisant des 'tags', similaires aux tags iterator-trait ( random_access_tag
et al). Cela fonctionne car une correspondance directe est préférable à une classe de base, ce qui est mieux qu'une classe de base d'une classe de base, 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>{});
}
Il existe d'autres méthodes couramment utilisées pour différencier les surcharges, telles que la correspondance exacte étant meilleure que la conversion, meilleure que les points de suspension.
Cependant, tag-dispatch peut s'étendre à un nombre illimité de choix et est un peu plus clair dans l'intention.