2017-06-16 2 views
17

Utiliser les enable_if de C++ 11 Je souhaite définir plusieurs implémentations spécialisées pour une fonction (basée sur le type du paramètre, disons) ainsi qu'une implémentation par défaut. Quelle est la bonne façon de le définir?Spécialisation de fonction de modèle partiel avec enable_if: implémenter l'implémentation par défaut

L'exemple suivant ne fonctionne pas comme prévu depuis l'appel de l'implémentation "générique", quel que soit le type T.

#include <iostream> 

template<typename T, typename Enable = void> 
void dummy(T t) 
{ 
    std::cout << "Generic: " << t << std::endl; 
} 


template<typename T, typename std::enable_if<std::is_integral<T>::value>::type> 
void dummy(T t) 
{ 
    std::cout << "Integral: " << t << std::endl; 
} 


template<typename T, typename std::enable_if<std::is_floating_point<T>::value>::type> 
void dummy(T t) 
{ 
    std::cout << "Floating point: " << t << std::endl; 
} 

int main() { 
    dummy(5); // Print "Generic: 5" 
    dummy(5.); // Print "Generic: 5" 
} 

Une solution dans mon exemple minimal consiste à déclarer explicitement la mise en œuvre « générique » ne pas pour les types de points entiers ni flottantes, en utilisant

std::enable_if<!std::is_integral<T>::value && !std::is_floating_point<T>::value>::type 

C'est exactement ce que je veux éviter, puisque dans mes cas d'utilisation réels il y a beaucoup d'implémentations spécialisées et je voudrais éviter une condition très longue (erreur sujette!) pour l'implémentation par défaut.

Répondre

11

La fonction ne peut pas être partiellement spécialisée. Je suppose que ce que vous voulez faire est de préférer ces surcharges qui contiennent des conditions explicites? Une façon d'y parvenir est d'utiliser des arguments variadique ellipsis dans la déclaration de la fonction default que la fonction de points de suspension ont une priorité inférieure en surcharge ordre de résolution:

#include <iostream> 

template<typename T> 
void dummy_impl(T t, ...) 
{ 
    std::cout << "Generic: " << t << std::endl; 
} 


template<typename T, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr> 
void dummy_impl(T t, int) 
{ 
    std::cout << "Integral: " << t << std::endl; 
} 


template<typename T, typename std::enable_if<std::is_floating_point<T>::value>::type* = nullptr> 
void dummy_impl(T t, int) 
{ 
    std::cout << "Floating point: " << t << std::endl; 
} 

template <class T> 
void dummy(T t) { 
    dummy_impl(t, int{}); 
} 

int main() { 
    dummy(5); 
    dummy(5.); 
    dummy("abc"); 
} 

Sortie:

Integral: 5 
Floating point: 5 
Generic: abc 

[live demo]

Une autre option comme mention @doublep dans le commentaire est l'utilisation de la structure avec l'implémentation de votre fonction et ensuite la spécialiser partiellement.

+3

Une autre option consiste à mettre en application une classe fictive/struct et se spécialisent en partie que. Bien que cela implique probablement plus de dactylographie. – doublep

+0

J'ai remarqué que l'on a vraiment besoin du second paramètre de template pour être un pointeur avec 'nullptr' par défaut pour que la construction fonctionne. Peux-tu m'expliquer pourquoi c'est le cas, ou m'indiquer une explication? Merci! – Bruno

+0

@doublep plus de dactylographie est parfaitement acceptable si cela signifie que tout le monde peut lire et maintenir votre code. Cela en vaut vraiment la peine. –

12

Vous pouvez introduire un rank de donner la priorité à certains de vos surcharges:

template <unsigned int N> 
struct rank : rank<N - 1> { }; 

template <> 
struct rank<0> { }; 

Vous pouvez ensuite définir vos dummy surcharges comme ceci:

template<typename T> 
void dummy(T t, rank<0>) 
{ 
    std::cout << "Generic: " << t << std::endl; 
} 

template<typename T, 
     typename std::enable_if<std::is_integral<T>::value>::type* = nullptr> 
void dummy(T t, rank<1>) 
{ 
    std::cout << "Integral: " << t << std::endl; 
} 

template<typename T, 
     typename std::enable_if<std::is_floating_point<T>::value>::type* = nullptr> 
void dummy(T t, rank<1>) 
{ 
    std::cout << "Floating point: " << t << std::endl; 
} 

Ensuite, vous pouvez masquer l'appel derrière un dispatch:

template <typename T> 
void dispatch(T t) 
{ 
    return dummy(t, rank<1>{}); 
} 

Utilisation:

int main() 
{ 
    dispatch(5); // Print "Integral: 5" 
    dispatch(5.); // Print "Floating point: 5" 
    dispatch("hi"); // Print "Generic: hi" 
} 

live example on wandbox


Explication:

L'utilisation rank introduit "priorité", car les conversions implicites sont nécessaires pour convertir un rank<X> à un rank<Y> quand X > Y. dispatch essaie d'abord d'appeler dummy avec rank<1>, donnant la priorité à vos surcharges contraints. Si enable_if échoue, rank<1> est implicitement converti en rank<0> et entre dans le cas de "repli".


Bonus: est ici une implémentation 17 C++ en utilisant if constexpr(...).

template<typename T> 
void dummy(T t) 
{ 
    if constexpr(std::is_integral_v<T>) 
    { 
     std::cout << "Integral: " << t << std::endl; 
    } 
    else if constexpr(std::is_floating_point_v<T>) 
    { 
     std::cout << "Floating point: " << t << std::endl; 
    } 
    else 
    { 
     std::cout << "Generic: " << t << std::endl; 
    } 
} 

live example on wandbox

+1

Belle solution, merci! – Bruno

+0

@Bruno Je ne suis pas d'accord, je pense que cette solution n'exprime pas l'intention d'envoyer à la bonne méthode. Lisez ce code quelques mois plus tard; Bien que ce soit assez simple, vous pourriez légitimement vous demander quelle est la signification de «rang». –

5

J'utiliser la balise envoi comme ceci:

namespace Details 
{ 
    namespace SupportedTypes 
    { 
     struct Integral {}; 
     struct FloatingPoint {}; 
     struct Generic {}; 
    }; 


    template <typename T, typename = void> 
    struct GetSupportedType 
    { 
     typedef SupportedTypes::Generic Type; 
    }; 

    template <typename T> 
    struct GetSupportedType< T, typename std::enable_if< std::is_integral<T>::value >::type > 
    { 
     typedef SupportedTypes::Integral Type; 
    }; 

    template <typename T> 
    struct GetSupportedType< T, typename std::enable_if< std::is_floating_point<T>::value >::type > 
    { 
     typedef SupportedTypes::FloatingPoint Type; 
    }; 

    template <typename T> 
    void dummy(T t, SupportedTypes::Generic) 
    { 
     std::cout << "Generic: " << t << std::endl; 
    } 

    template <typename T> 
    void dummy(T t, SupportedTypes::Integral) 
    { 
     std::cout << "Integral: " << t << std::endl; 
    } 

    template <typename T> 
    void dummy(T t, SupportedTypes::FloatingPoint) 
    { 
     std::cout << "Floating point: " << t << std::endl; 
    } 
} // namespace Details 

Et puis cacher le code de la plaque de la chaudière comme ceci:

template <typename T> 
void dummy(T t) 
{ 
    typedef typename Details::GetSupportedType<T>::Type SupportedType; 
    Details::dummy(t, SupportedType()); 
} 

GetSupportedType vous donne une façon centrale deviner le type réel que vous allez utiliser, c'est celui que vous w ant pour se spécialiser à chaque fois que vous ajoutez un nouveau type.

Ensuite, vous venez d'appeler la surcharge dummy droite en fournissant une instance de la balise droite.

Enfin, invoquez dummy:

dummy(5); // Print "Generic: 5" 
dummy(5.); // Print "Floating point: 5" 
dummy("lol"); // Print "Generic: lol"