Pourquoi devrais-je éviter std :: enable_if dans les signatures de fonction

165

Scott Meyers a publié le contenu et le statut de son prochain livre EC ++ 11. Il a écrit qu'un élément du livre pourrait être "Eviter std::enable_ifles signatures de fonction" .

std::enable_if peut être utilisé comme argument de fonction, comme type de retour ou comme modèle de classe ou paramètre de modèle de fonction pour supprimer conditionnellement des fonctions ou des classes de la résolution de surcharge.

Dans cette question, les trois solutions sont présentées.

En tant que paramètre de fonction:

template<typename T>
struct Check1
{
   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, int>::value >::type* = 0) { return 42; }

   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, double>::value >::type* = 0) { return 3.14; }   
};

En tant que paramètre de modèle:

template<typename T>
struct Check2
{
   template<typename U = T, typename std::enable_if<
            std::is_same<U, int>::value, int>::type = 0>
   U read() { return 42; }

   template<typename U = T, typename std::enable_if<
            std::is_same<U, double>::value, int>::type = 0>
   U read() { return 3.14; }   
};

Comme type de retour:

template<typename T>
struct Check3
{
   template<typename U = T>
   typename std::enable_if<std::is_same<U, int>::value, U>::type read() {
      return 42;
   }

   template<typename U = T>
   typename std::enable_if<std::is_same<U, double>::value, U>::type read() {
      return 3.14;
   }   
};
  • Quelle solution privilégier et pourquoi éviter les autres?
  • Dans quels cas "Eviter std::enable_ifdans les signatures de fonction" concerne l'utilisation comme type de retour (qui ne fait pas partie de la signature de fonction normale mais des spécialisations de modèle)?
  • Existe-t-il des différences entre les modèles de fonctions membres et non membres?
hansmaad
la source
Parce que la surcharge est tout aussi agréable, généralement. Si quoi que ce soit, déléguez à une implémentation qui utilise des modèles de classe (spécialisés).
sehe
Les fonctions membres diffèrent en ce que l'ensemble de surcharge inclut les surcharges déclarées après la surcharge actuelle. Ceci est particulièrement important lorsque vous faites variadics retard type de retour (où le type de retour doit être déduite d' une autre surcharge)
sehe
1
Eh bien, simplement subjectivement, je dois dire que, bien que souvent très utile, je n'aime pas std::enable_ifencombrer mes signatures de fonction (en particulier la version laide de l' nullptrargument de fonction supplémentaire ) parce que cela ressemble toujours à ce que c'est, un étrange hack (pour quelque chose qui static ifpourrait faire beaucoup plus beau et propre) en utilisant un modèle de magie noire pour exploiter une fonctionnalité de langage intéressante. C'est pourquoi je préfère l'envoi de balises chaque fois que possible (enfin, vous avez encore des arguments étranges supplémentaires, mais pas dans l'interface publique et aussi beaucoup moins moche et cryptique ).
Christian Rau
2
Je veux demander ce que =0dans typename std::enable_if<std::is_same<U, int>::value, int>::type = 0accomplir? Je n'ai pas trouvé de ressources correctes pour le comprendre. Je sais que la première partie avant =0a un type de membre intsi Uet intest le même. Merci beaucoup!
astroboylrx
4
@astroboylrx Drôle, j'allais juste mettre un commentaire en notant cela. Fondamentalement, cela = 0 indique qu'il s'agit d'un paramètre de modèle non type par défaut . C'est fait de cette façon car les paramètres de modèle de type par défaut ne font pas partie de la signature, vous ne pouvez donc pas les surcharger.
Nir Friedman

Réponses:

107

Mettez le hack dans les paramètres du modèle .

L' enable_ifapproche des paramètres sur modèle présente au moins deux avantages par rapport aux autres:

  • lisibilité : l'utilisation de enable_if et les types de retour / d'argument ne sont pas fusionnés en un seul morceau désordonné de désambiguïsateurs de nom de type et d'accès de type imbriqués; même si l'encombrement du désambiguïsateur et du type imbriqué peut être atténué avec des modèles d'alias, cela fusionnerait toujours deux éléments non liés ensemble. L'utilisation de enable_if est liée aux paramètres du modèle et non aux types de retour. Les avoir dans les paramètres du modèle signifie qu'ils sont plus proches de ce qui compte;

  • applicabilité universelle : les constructeurs n'ont pas de types de retour, et certains opérateurs ne peuvent pas avoir d'arguments supplémentaires, donc aucune des deux autres options ne peut être appliquée partout. Mettre enable_if dans un paramètre de modèle fonctionne partout car vous ne pouvez de toute façon utiliser SFINAE que sur des modèles.

Pour moi, l'aspect lisibilité est le grand facteur de motivation dans ce choix.

R. Martinho Fernandes
la source
4
L'utilisation de la FUNCTION_REQUIRESmacro ici la rend beaucoup plus agréable à lire, et elle fonctionne également dans les compilateurs C ++ 03, et elle repose sur l'utilisation enable_ifdans le type de retour. De plus, l'utilisation enable_ifde paramètres de modèle de fonction entraîne des problèmes de surcharge, car désormais la signature de la fonction n'est plus unique, ce qui entraîne des erreurs de surcharge ambiguës.
Paul Fultz II
3
C'est une vieille question, mais pour tous ceux qui lisent encore: la solution au problème soulevé par @Paul est de l'utiliser enable_ifavec un paramètre de modèle non type par défaut, ce qui permet la surcharge. Ie enable_if_t<condition, int> = 0au lieu de typename = enable_if_t<condition>.
Nir Friedman
lien de retour vers presque-statique-if: web.archive.org/web/20150726012736/http
//flamingdangerzone.com
@ R.MartinhoFernandes le flamingdangerzonelien dans votre commentaire semble conduire à une page d'installation de logiciels espions maintenant. Je l'ai signalé à l'attention du modérateur.
nispio
58

std::enable_ifrepose sur le principe « L'échec de la sous-station n'est pas une erreur » (alias SFINAE) lors de la déduction des arguments de modèle . Il s'agit d'une fonctionnalité de langage très fragile et vous devez faire très attention pour bien faire les choses.

  1. si votre condition à l'intérieur de enable_ifcontient un modèle imbriqué ou une définition de type (indice: recherchez des ::jetons), alors la résolution de ces tempatles ou types imbriqués est généralement un contexte non déduit . Tout échec de substitution sur un tel contexte non déduit est une erreur .
  2. les diverses conditions dans de multiples enable_ifsurcharges ne peuvent pas avoir de chevauchement car la résolution de surcharge serait ambiguë. C'est quelque chose que vous devez vérifier vous-même en tant qu'auteur, même si vous obtenez de bons avertissements du compilateur.
  3. enable_ifmanipule l'ensemble des fonctions viables pendant la résolution de surcharge qui peuvent avoir des interactions surprenantes en fonction de la présence d'autres fonctions qui sont importées d'autres portées (par exemple via ADL). Cela le rend peu robuste.

En bref, quand cela fonctionne, cela fonctionne, mais quand ce n'est pas le cas, cela peut être très difficile à déboguer. Une très bonne alternative est d'utiliser la distribution de balises , c'est-à-dire de déléguer à une fonction d'implémentation (généralement dans un detailespace de noms ou dans une classe d'assistance) qui reçoit un argument factice basé sur la même condition de compilation que vous utilisez dans le enable_if.

template<typename T>
T fun(T arg) 
{ 
    return detail::fun(arg, typename some_template_trait<T>::type() ); 
}

namespace detail {
    template<typename T>
    fun(T arg, std::false_type /* dummy */) { }

    template<typename T>
    fun(T arg, std::true_type /* dummy */) {}
}

La distribution de balises ne manipule pas l'ensemble de surcharge, mais vous aide à sélectionner exactement la fonction que vous voulez en fournissant les arguments appropriés via une expression à la compilation (par exemple dans un trait de type). D'après mon expérience, c'est beaucoup plus facile à déboguer et à faire correctement. Si vous êtes un rédacteur de bibliothèque en herbe de traits de type sophistiqués, vous en aurez peut-être besoin enable_if, mais pour la plupart des utilisations régulières des conditions de compilation, ce n'est pas recommandé.

TemplateRex
la source
22
La répartition des balises présente cependant un inconvénient: si vous avez un trait qui détecte la présence d'une fonction et que cette fonction est implémentée avec l'approche de répartition des balises, elle signale toujours ce membre comme présent et entraîne une erreur au lieu d'un échec potentiel de substitution. . SFINAE est principalement une technique pour supprimer les surcharges des ensembles candidats, et la distribution de balises est une technique permettant de sélectionner entre deux (ou plus) surcharges. Il existe un certain chevauchement des fonctionnalités, mais elles ne sont pas équivalentes.
R. Martinho Fernandes
@ R.MartinhoFernandes pouvez-vous donner un court exemple et illustrer comment enable_ifle faire correctement?
TemplateRex
1
@ R.MartinhoFernandes Je pense qu'une réponse distincte expliquant ces points pourrait ajouter de la valeur au PO. :-) BTW, écrire des traits comme is_f_ableest quelque chose que je considère comme une tâche pour les rédacteurs de bibliothèques qui peuvent bien sûr utiliser SFINAE quand cela leur donne un avantage, mais pour les utilisateurs "réguliers" et étant donné un trait is_f_able, je pense que l'envoi de balises est plus facile.
TemplateRex
1
@hansmaad J'ai posté une courte réponse à votre question, et je vais aborder la question de "à SFINAE ou à ne pas SFINAE" dans un article de blog à la place (c'est un peu hors sujet sur cette question). Dès que j'ai le temps de le terminer, je veux dire.
R. Martinho Fernandes
8
SFINAE est "fragile"? Quoi?
Courses de légèreté en orbite le
5

Quelle solution privilégier et pourquoi éviter les autres?

  • Le paramètre de modèle

    • Il est utilisable dans les constructeurs.
    • Il est utilisable dans l'opérateur de conversion défini par l'utilisateur.
    • Il nécessite C ++ 11 ou version ultérieure.
    • C'est l'OMI, le plus lisible.
    • Il peut facilement être mal utilisé et générer des erreurs avec des surcharges:

      template<typename T, typename = std::enable_if_t<std::is_same<T, int>::value>>
      void f() {/*...*/}
      
      template<typename T, typename = std::enable_if_t<std::is_same<T, float>::value>>
      void f() {/*...*/} // Redefinition: both are just template<typename, typename> f()

    Avis typename = std::enable_if_t<cond>au lieu de corrigerstd::enable_if_t<cond, int>::type = 0

  • type de retour:

    • Il ne peut pas être utilisé dans le constructeur. (pas de type de retour)
    • Il ne peut pas être utilisé dans l'opérateur de conversion défini par l'utilisateur. (non déductible)
    • Il peut être utilisé avant C ++ 11.
    • Deuxième IMO plus lisible.
  • Enfin, dans le paramètre de fonction:

    • Il peut être utilisé avant C ++ 11.
    • Il est utilisable dans les constructeurs.
    • Il ne peut pas être utilisé dans l'opérateur de conversion défini par l'utilisateur. (pas de paramètres)
    • Il ne peut pas être utilisée dans des procédés avec un nombre fixe d'arguments (opérateurs unaires / binaires +, -, *, ...)
    • Il peut être utilisé en toute sécurité en héritage (voir ci-dessous).
    • Changer la signature de la fonction (vous avez fondamentalement un supplément comme dernier argument void* = nullptr) (donc le pointeur de fonction serait différent, et ainsi de suite)

Existe-t-il des différences entre les modèles de fonctions membres et non membres?

Il existe des différences subtiles avec l'héritage et using:

Selon le using-declarator(c'est moi qui souligne):

namespace.udecl

L'ensemble des déclarations introduites par le déclarateur using est trouvé en effectuant une recherche de nom qualifié ([basic.lookup.qual], [class.member.lookup]) pour le nom dans le déclarateur using, en excluant les fonctions qui sont masquées comme décrit au dessous de.

...

Lorsqu'un déclarateur utilisant apporte des déclarations d'une classe de base dans une classe dérivée, les fonctions membres et les modèles de fonctions membres dans la classe dérivée remplacent et / ou masquent les fonctions membres et les modèles de fonctions membres avec le même nom, paramètre-type-list, cv- qualification, et ref-qualifier (le cas échéant) dans une classe de base (plutôt que conflictuelle). Ces déclarations masquées ou remplacées sont exclues de l'ensemble des déclarations introduites par le déclarateur using.

Ainsi, pour l'argument de modèle et le type de retour, les méthodes sont masquées est le scénario suivant:

struct Base
{
    template <std::size_t I, std::enable_if_t<I == 0>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 0> g() {}
};

struct S : Base
{
    using Base::f; // Useless, f<0> is still hidden
    using Base::g; // Useless, g<0> is still hidden

    template <std::size_t I, std::enable_if_t<I == 1>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 1> g() {}
};

Démo (gcc trouve mal la fonction de base).

Alors qu'avec un argument, un scénario similaire fonctionne:

struct Base
{
    template <std::size_t I>
    void h(std::enable_if_t<I == 0>* = nullptr) {}
};

struct S : Base
{
    using Base::h; // Base::h<0> is visible

    template <std::size_t I>
    void h(std::enable_if_t<I == 1>* = nullptr) {}
};

Démo

Jarod42
la source