web-dev-qa-db-fra.com

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

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

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 de manière conditionnelle des fonctions ou des classes de la résolution de surcharge.

Dans cette question les trois solutions sont montré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; }   
};

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 faut-il préférer et pourquoi devrais-je éviter les autres?
  • Dans quels cas "Evitez std::enable_if dans les signatures de fonction " concerne-t-il l’utilisation en tant que 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 fonction membres et non membres?
160
hansmaad

Mettez le hack dans les paramètres du template.

L'approche de paramètre enable_if Sur modèle présente au moins deux avantages par rapport aux autres:

  • lisibilité: les types enable_if et return/argument ne sont pas fusionnés en un seul bloc en désordre contenant des noms propres et des accès de type imbriqués; Même si le fouillis de l’homonymie et du type imbriqué peut être atténué à l’aide de modèles de pseudonymes, il serait toujours possible de fusionner deux éléments non liés. L'utilisation de enable_if est liée aux paramètres du modèle, pas 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 ne disposent pas de types de retour et certains opérateurs ne peuvent pas avoir d'arguments supplémentaires. Aucune des deux autres options ne peut donc être appliquée partout. Mettre enable_if dans un paramètre de modèle fonctionne partout car vous ne pouvez utiliser SFINAE que sur les modèles.

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

103

std::enable_if S'appuie sur le principe " . L'échec de la substitution n'est pas une erreur " (aka SFINAE) pendant déduction d'argument de modèle . Il s’agit d’une fonctionnalité de langage très fragile et vous devez faire très attention à bien le faire.

  1. si votre condition à l'intérieur du enable_if contient un modèle imbriqué ou une définition de type (indice: recherchez les jetons ::), la résolution de ces types de tempatles ou 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 plusieurs surcharges enable_if ne peuvent pas se chevaucher car la résolution de la surcharge serait ambiguë. En tant qu’auteur, c’est quelque chose que vous devez vérifier vous-même, même si vous obtenez de bons avertissements pour le compilateur.
  3. enable_if Manipule l'ensemble des fonctions viables pendant la résolution de la surcharge, ce qui peut avoir des interactions surprenantes en fonction de la présence d'autres fonctions importées d'autres portées (par exemple, via ADL). Cela le 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 consiste à utiliser la répartition des balises , c’est-à-dire à déléguer à une fonction d’implémentation (généralement dans un espace de noms detail ou dans un assistant). class) qui reçoit un argument factice basé sur la même condition de compilation que celle que vous utilisez dans 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 des balises ne manipule pas le jeu de surcharge, mais vous aide à sélectionner exactement la fonction souhaitée en fournissant les arguments appropriés via une expression de compilation (par exemple, dans un trait de caractère). D'après mon expérience, c'est beaucoup plus facile à déboguer et à corriger. Si vous êtes un écrivain en herbe possédant des caractères typographiques sophistiqués, vous aurez peut-être besoin de enable_if, Mais cela n'est pas recommandé pour la plupart des conditions de compilation habituelles.

55
TemplateRex

Quelle solution faut-il préférer et pourquoi devrais-je éviter les autres?

  • Le paramètre template

    • 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.
    • C'est IMO, le plus lisible.
    • Il pourrait facilement être utilisé à tort et 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()
      

    Remarquer typename = std::enable_if_t<cond> au lieu de correct std::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 un opérateur de conversion défini par l'utilisateur. (non déductible)
    • Il peut être utilisé pré-C++ 11.
    • Deuxième IMO plus lisible.
  • Enfin, en paramètre de fonction:

    • Il peut être utilisé pré-C++ 11.
    • Il est utilisable dans les constructeurs.
    • Il ne peut pas être utilisé dans un opérateur de conversion défini par l'utilisateur. (pas de paramètres)
    • Il ne peut pas être utilisé dans des méthodes à nombre d'arguments fixe (opérateurs unaires/binaires +, -, *, ...)
    • Il peut être utilisé sans risque en héritage (voir ci-dessous).
    • Changer la signature de la fonction (vous avez essentiellement un extra comme dernier argument void* = nullptr) (le pointeur de fonction serait donc différent, etc.)

Existe-t-il des différences entre les modèles de fonction 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 de déclarations introduit par using-declarator est trouvé en effectuant une recherche de nom qualifié ([basic.lookup.qual], [class.member.lookup]) pour le nom figurant dans using-declarator, à l'exception des fonctions masquées comme décrit au dessous de.

...

Using-declarator introduit les déclarations d'une classe de base dans une classe dérivée, fonctions membres et modèles de fonctions membres dans la classe dérivée substituant et/ou masquant les fonctions membres et les modèles de fonctions membres portant 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 contradictoires). Ces déclarations masquées ou remplacées sont exclues de l'ensemble des déclarations introduites par le déclarant-using.

Ainsi, pour les arguments de modèle et le type de retour, les méthodes sont masqué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() {}
};

Demo (gcc trouve à tort 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

4
Jarod42