Approches pour fonctionner SFINAE en C ++

40

J'utilise beaucoup la fonction SFINAE dans un projet et je ne sais pas s'il existe des différences entre les deux approches suivantes (autres que le style):

#include <cstdlib>
#include <type_traits>
#include <iostream>

template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo()
{
    std::cout << "method 1" << std::endl;
}

template <class T, std::enable_if_t<std::is_same_v<T, double>>* = 0>
void foo()
{
    std::cout << "method 2" << std::endl;
}

int main()
{
    foo<int>();
    foo<double>();

    std::cout << "Done...";
    std::getchar();

    return EXIT_SUCCESS;
}

La sortie du programme est comme prévu:

method 1
method 2
Done...

J'ai vu la méthode 2 utilisée plus souvent dans stackoverflow, mais je préfère la méthode 1.

Y a-t-il des circonstances où ces deux approches diffèrent?

Keith
la source
Comment exécutez-vous ce programme? Cela ne se compilera pas pour moi.
alter igel
@alter igel il aura besoin d'un compilateur C ++ 17. J'ai utilisé MSVC 2019 pour tester cet exemple mais je travaille principalement avec Clang.
keith
En relation: pourquoi-devrais-je-éviter-les signatures stdenable-if-in-function et C ++ 20 introduit également de nouvelles façons avec le concept :-)
Jarod42
@ Jarod42 Les concepts sont l'une des choses les plus nécessaires pour moi à partir de C ++ 20.
val dit Réintégrer Monica

Réponses:

35

J'ai vu la méthode 2 utilisée plus souvent dans stackoverflow, mais je préfère la méthode 1.

Suggestion: préférez la méthode 2.

Les deux méthodes fonctionnent avec des fonctions uniques. Le problème se pose lorsque vous avez plus d'une fonction, avec la même signature, et que vous ne souhaitez activer qu'une seule fonction de l'ensemble.

Supposons que vous voulez activer foo(), version 1, quand bar<T>()(faire semblant que c'est une constexprfonction) est true, et foo(), la version 2, quand bar<T>()est false.

Avec

template <typename T, typename = std::enable_if_t<true == bar<T>()>>
void foo () // version 1
 { }

template <typename T, typename = std::enable_if_t<false == bar<T>()>>
void foo () // version 2
 { }

vous obtenez une erreur de compilation car vous avez une ambiguïté: deux foo()fonctions avec la même signature (un paramètre de modèle par défaut ne change pas la signature).

Mais la solution suivante

template <typename T, std::enable_if_t<true == bar<T>(), bool> = true>
void foo () // version 1
 { }

template <typename T, std::enable_if_t<false == bar<T>(), bool> = true>
void foo () // version 2
 { }

fonctionne, car SFINAE modifie la signature des fonctions.

Observation indépendante: il existe également une troisième méthode: activer / désactiver le type de retour (sauf pour les constructeurs de classe / struct, évidemment)

template <typename T>
std::enable_if_t<true == bar<T>()> foo () // version 1
 { }

template <typename T>
std::enable_if_t<false == bar<T>()> foo () // version 2
 { }

En tant que méthode 2, la méthode 3 est compatible avec la sélection de fonctions alternatives avec la même signature.

max66
la source
1
Merci pour la grande explication, je préférerai les méthodes 2 et 3 à partir de maintenant :-)
keith
"un paramètre de modèle par défaut ne change pas la signature" - en quoi est-ce différent dans votre deuxième variante, qui utilise également des paramètres de modèle par défaut?
Eric
1
@Eric - Pas simple à dire ... Je suppose que l'autre réponse l'explique mieux ... Si SFINAE active / désactive l'argument de modèle par défaut, la foo()fonction reste disponible lorsque vous l'appelez avec un deuxième paramètre de modèle explicite (l' foo<double, double>();appel). Et si rester disponible, il y a une ambiguïté avec l'autre version. Avec la méthode 2, SFINAE active / désactive le deuxième argument, pas le paramètre par défaut. Vous ne pouvez donc pas l'appeler en expliquant le paramètre car il y a un échec de substitution qui ne permet pas un deuxième paramètre. Donc la version n'est pas disponible, donc pas d'ambiguïté
max66
3
La méthode 3 a l'avantage supplémentaire de ne généralement pas fuir dans le nom du symbole. La variante auto foo() -> std::enable_if_t<...>est souvent utile pour éviter de masquer la signature de fonction et pour permettre l'utilisation des arguments de fonction.
Deduplicator
@ max66: le point clé est donc que l'échec de la substitution dans un paramètre de modèle par défaut n'est pas une erreur si le paramètre est fourni et qu'aucun défaut n'est nécessaire?
Eric
21

En plus de la réponse de max66 , une autre raison de préférer la méthode 2 est qu'avec la méthode 1, vous pouvez (accidentellement) passer un paramètre de type explicite comme deuxième argument de modèle et vaincre complètement le mécanisme SFINAE. Cela peut se produire comme une faute de frappe, une erreur de copier / coller ou comme une erreur dans un mécanisme de modèle plus grand.

#include <cstdlib>
#include <type_traits>
#include <iostream>

// NOTE: foo should only accept T=int
template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo(){
    std::cout << "method 1" << std::endl;
}

int main(){

    // works fine
    foo<int>();

    // ERROR: subsitution failure, as expected
    // foo<double>();

    // Oops! also works, even though T != int :(
    foo<double, double>();

    return 0;
}

Démo en direct ici

alter igel
la source
Bon point. Le mécanisme peut être détourné.
max66