173 votes

Pourquoi dois-je éviter std::enable_if dans les signatures de fonctions ?

Scott Meyers a publié contenu et statut de son prochain livre EC++11. Il a écrit qu'un élément du livre pourrait être _"Évitez std::enable_if dans les signatures de fonctions"_ .

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

Sur cette question les trois solutions sont présentées.

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

Comme 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 faut-il privilégier et pourquoi faut-il éviter les autres ?
  • Dans quels cas _"Évitez std::enable_if dans les signatures de fonctions"_ concerne l'utilisation comme type de retour (qui ne fait pas partie de la signature normale de la fonction mais des spécialisations du modèle) ?
  • Y a-t-il des différences entre les modèles de fonctions pour les membres et les non-membres ?

0 votes

Parce que la surcharge est tout aussi agréable, généralement. Dans tous les cas, déléguez à une implémentation qui utilise des modèles de classe (spécialisés).

0 votes

Les fonctions membres se distinguent par le fait que le jeu de surcharges comprend les surcharges déclarées. après la surcharge de courant. Ceci est particulièrement important lorsque l'on utilise un type de retour différé variadique (où le type de retour doit être déduit d'une autre surcharge).

1 votes

Eh bien, de manière purement subjective, je dois dire que, bien qu'il soit souvent très utile, je n'aime pas std::enable_if pour encombrer mes signatures de fonctions (en particulier l'affreux ajout de nullptr version de l'argument de la fonction) parce qu'il ressemble toujours à ce qu'il est, un hack étrange (pour quelque chose qu'un static if pourrait faire beaucoup plus beau et propre) en utilisant la magie noire des modèles pour exploiter une caractéristique intéressante du langage. C'est pourquoi je préfère le tag-dispatching quand c'est possible (bien, vous avez toujours des arguments supplémentaires étranges, mais pas dans l'interface publique et aussi beaucoup moins de laid et cryptique ).

113voto

R. Martinho Fernandes Points 96873

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

El enable_if L'approche fondée sur les paramètres des modèles présente au moins deux avantages par rapport aux autres :

  • lisibilité l'utilisation de enable_if et les types de retour/argument ne sont pas fusionnés en un seul morceau désordonné de désambiguïsateurs typename et d'accès à des types imbriqués ; même si le désambiguïsateur et le type imbriqué peuvent être atténués avec des modèles d'alias, cela fusionnerait encore deux choses sans rapport. 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 importe ;

  • application 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 puisque vous pouvez seulement utiliser SFINAE sur les modèles de toute façon.

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

4 votes

Utilisation de la FUNCTION_REQUIRES macro ici Il fonctionne également avec les compilateurs C++03 et repose sur l'utilisation de l'option enable_if dans le type de retour. De même, l'utilisation de enable_if dans les paramètres des modèles de fonctions pose des problèmes de surcharge, car maintenant la signature de la fonction n'est pas unique, ce qui entraîne des erreurs de surcharge ambiguës.

3 votes

Il s'agit d'une vieille question, mais pour ceux qui lisent encore : la solution au problème soulevé par @Paul est d'utiliser enable_if avec un paramètre de modèle non-type par défaut, ce qui permet la surcharge. C'est-à-dire enable_if_t<condition, int> = 0 au lieu de typename = enable_if_t<condition> .

0 votes

58voto

TemplateRex Points 26447

std::enable_if s'appuie sur le " L'échec de la substitution n'est pas une erreur " (alias SFINAE) pendant déduction d'un argument modèle . Il s'agit d'un très fragile et vous devez faire très attention à ce qu'elle soit correcte.

  1. si votre condition à l'intérieur de la enable_if contient un modèle ou une définition de type imbriqué (indice : cherchez l'option :: ), alors la résolution de ces tempatles ou types imbriqués est habituellement un contexte non scolarisé . Tout échec de substitution sur un tel contexte non éduqué est une erreur .
  2. les différentes conditions dans de multiples enable_if les surcharges ne peuvent pas avoir de chevauchement car la résolution des surcharges serait ambiguë. C'est quelque chose que vous devez vérifier vous-même en tant qu'auteur, bien que vous receviez de bons avertissements du compilateur.
  3. enable_if manipule l'ensemble des fonctions viables pendant la résolution des surcharges, ce qui peut avoir des interactions surprenantes en fonction de la présence d'autres fonctions provenant d'autres scopes (par exemple, via ADL). Cela ne la rend pas très robuste.

En bref, quand ça marche, ça marche, mais quand ça ne marche pas, ça peut être très difficile à déboguer. Une très bonne alternative est d'utiliser répartition des étiquettes c'est-à-dire de déléguer à une fonction d'implémentation (généralement dans un fichier detail ou dans une classe d'aide) qui reçoit un argument fictif en fonction de la même condition de compilation que celle utilisée dans la méthode d'évaluation de la qualité de l'eau. 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 répartition par étiquette ne manipule pas l'ensemble des surcharges, mais vous aide à sélectionner exactement la fonction que vous voulez en fournissant les arguments appropriés par le biais d'une expression de compilation (par exemple, dans un trait de type). D'après mon expérience, cette méthode est beaucoup plus facile à déboguer et à mettre en œuvre. Si vous aspirez à écrire des traits de type sophistiqués, vous pourriez avoir besoin des éléments suivants enable_if d'une manière ou d'une autre, mais pour l'utilisation la plus régulière des conditions de compilation, ce n'est pas recommandé.

22 votes

La répartition par étiquette a 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 par étiquette, elle signalera toujours ce membre comme présent, et résultera en une erreur au lieu d'un échec potentiel de substitution. SFINAE est principalement une technique pour supprimer les surcharges des ensembles de candidats, et le tag dispatching est une technique pour sélectionner entre deux (ou plus) surcharges. Il y a un certain chevauchement dans la fonctionnalité, mais ils ne sont pas équivalents.

0 votes

@R.MartinhoFernandes pouvez-vous donner un court exemple, et illustrer comment enable_if ferait bien les choses ?

0 votes

@R.MartinhoFernandes J'ai mis à jour ma réponse pour refléter un peu votre commentaire, tnx.

8voto

Jarod42 Points 15729

Quelle solution faut-il privilégier et pourquoi faut-il éviter les autres ?

Option 1 : enable_if dans le paramètre du 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 une version ultérieure.

  • A mon avis, c'est le plus lisible.

  • Il est facile de mal utiliser et de produire 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()

    Remarquez l'utilisation de typename = std::enable_if_t<cond> au lieu de la bonne std::enable_if_t<cond, int>::type = 0

Option 2 : enable_if dans le type de retour

  • Il ne peut pas être utilisé avec les constructeurs (qui n'ont pas de type de retour).
  • Il ne peut pas être utilisé dans un opérateur de conversion défini par l'utilisateur (car il n'est pas déductible).
  • Il peut être utilisé avant C++11.
  • Le second est plus lisible.

Option 3 : enable_if dans un paramètre de fonction

  • Il peut être utilisé avant C++11.
  • Il est utilisable dans les constructeurs.
  • Il ne peut pas être utilisé dans les opérateurs de conversion définis par l'utilisateur (ils n'ont pas de paramètres).
  • Il ne peut pas être utilisé dans les méthodes ayant un nombre fixe d'arguments, comme les opérateurs unaires/binaires. + , - , * et autres.
  • Il peut être utilisé sans danger pour l'héritage (voir ci-dessous).
  • Change la signature de la fonction (vous avez fondamentalement un extra comme dernier argument). void* = nullptr ) ; cela entraîne un comportement différent des pointeurs de la fonction et ainsi de suite.

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

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

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

espace-nom.udecl

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

...

Lorsqu'un déclarateur d'utilisation introduit des déclarations d'une classe de base dans une classe dérivée, les fonctions membres et les modèles de fonctions membres de la classe dérivée remplacent et/ou masquent les fonctions membres et les modèles de fonctions membres. avec le même nom, la même liste de types de paramètres, la même qualification cv et le même qualificatif ref (le cas échéant) dans une classe de base. (plutôt que contradictoires). De telles déclarations cachées ou surchargées sont exclues de l'ensemble des déclarations introduites par le déclarateur using-declarator.

Ainsi, tant pour l'argument du modèle que pour le type de retour, les méthodes sont cachées dans 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 l'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

0voto

MSalters Points 74024

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

Quand la question a été posée, std::enable_if de <type_traits> était le meilleur outil disponible, et les autres réponses sont raisonnables jusqu'à C++17.

Aujourd'hui, dans C++20, nous avons un support direct du compilateur via requires .

#include <concepts
template<typename T>
struct Check20
{
   template<typename U = T>
   U read() requires std::same_as <U, int>
   { return 42; }

   template<typename U = T>
   U read() requires std::same_as <U, double>
   { return 3.14; }   
};

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X