c++ - les - langage compilé définition



Que font les compilateurs avec les branchements au moment de la compilation? (3)

EDIT: J'ai pris le cas "if / else" comme exemple pouvant parfois être résolu au moment de la compilation (par exemple lorsque des valeurs statiques sont impliquées, cf <type_traits> ). L'adaptation des réponses ci-dessous à d'autres types de branchements statiques (par exemple, des branches multiples ou des branches multicritères) devrait être simple. Notez que le branchement au moment de la compilation utilisant la programmation meta-template n'est pas le sujet ici.

Dans un code typique comme celui-ci

#include <type_traits>

template <class T>
T numeric_procedure( const T& x )
{
    if ( std::is_integral<T>::value )
    {
        // Integral types
    }
    else
    {
        // Floating point numeric types
    }
}

Le compilateur optimisera-t-il l'instruction if / else lorsque je définirai ultérieurement des types de modèles spécifiques dans mon code?

Une alternative simple serait d'écrire quelque chose comme ceci:

#include <type_traits>

template <class T>
inline T numeric_procedure( const T& x )
{
    return numeric_procedure_impl( x, std::is_integral<T>() );
}

// ------------------------------------------------------------------------

template <class T>
T numeric_procedure_impl( const T& x, std::true_type const )
{
    // Integral types
}

template <class T>
T numeric_procedure_impl( const T& x, std::false_type const )
{
    // Floating point numeric types
}

Existe-t-il une différence de performance entre ces solutions? Y a-t-il des raisons non subjectives de dire que l'une est meilleure que l'autre? Existe-t-il d'autres solutions (éventuellement meilleures) pour gérer les branchements au moment de la compilation?


Answer #1

TL; DR

Il existe plusieurs façons d'obtenir un comportement d'exécution différent en fonction d'un paramètre de modèle. La performance ne devrait pas être votre principale préoccupation, mais la flexibilité et la maintenabilité devraient l'être. Dans tous les cas, les différents wrappers minces et les expressions conditionnelles constantes seront tous optimisés sur tout compilateur décent pour les versions release. Ci-dessous un petit résumé avec les différents compromis (inspiré par cette réponse de @AndyProwl).

Temps d'exécution si

Votre première solution est la simple exécution if :

template<class T>
T numeric_procedure(const T& x)
{
    if (std::is_integral<T>::value) {
        // valid code for integral types
    } else {
        // valid code for non-integral types,
        // must ALSO compile for integral types
    }
}

C'est simple et efficace: tout compilateur décent optimisera la branche morte.

Il y a plusieurs inconvénients:

  • Sur certaines plates-formes (MSVC), une expression conditionnelle constante génère un avertissement de compilateur erroné que vous devez ensuite ignorer ou masquer.
  • Mais pire encore, sur toutes les plates-formes conformes, les deux branches de l'instruction if/else doivent réellement être compilées pour tous les types T , même si l'une des branches est inconnue. Si T contient différents types de membres en fonction de sa nature, vous obtiendrez une erreur de compilation dès que vous tenterez d'y accéder.

Tag dispatching

Votre deuxième approche est connue sous le nom de tag-dispatching:

template<class T>
T numeric_procedure_impl(const T& x, std::false_type)
{
    // valid code for non-integral types,
    // CAN contain code that is invalid for integral types
}    

template<class T>
T numeric_procedure_impl(const T& x, std::true_type)
{
    // valid code for integral types
}

template<class T>
T numeric_procedure(const T& x)
{
    return numeric_procedure_impl(x, std::is_integral<T>());
}

Cela fonctionne bien, sans surcharge d'exécution: le std::is_integral<T>() temporaire std::is_integral<T>() et l'appel à la fonction d'assistance à une ligne seront tous deux optimisés sur n'importe quelle plate-forme décente.

Le principal inconvénient (mineur de l’OMI) est que vous avez une fonction standard avec 3 fonctions au lieu de 1.

SFINAE

SFINAE (l’échec de la substitution n’est pas une erreur)

template<class T, class = typename std::enable_if<!std::is_integral<T>::value>::type>
T numeric_procedure(const T& x)
{
    // valid code for non-integral types,
    // CAN contain code that is invalid for integral types
}    

template<class T, class = typename std::enable_if<std::is_integral<T>::value>::type>
T numeric_procedure(const T& x)
{
    // valid code for integral types
}

Cela a le même effet que le tag-dispatching mais fonctionne légèrement différemment. Au lieu d'utiliser la déduction d'argument pour sélectionner la surcharge auxiliaire appropriée, elle manipule directement l'ensemble de surcharge pour votre fonction principale.

L'inconvénient est que cela peut être un moyen fragile et délicat si vous ne savez pas exactement ce qu'est l'ensemble de surcharge (par exemple, avec un code lourd, ADL pourrait générer plus de surcharges à partir des espaces de noms associés ). Et comparé à la distribution de tags, la sélection basée sur autre chose qu'une décision binaire est beaucoup plus complexe.

Spécialisation partielle

Une autre approche consiste à utiliser un assistant de modèle de classe avec un opérateur d'application de fonction et à le spécialiser partiellement.

template<class T, bool> 
struct numeric_functor;

template<class T>
struct numeric_functor<T, false>
{
    T operator()(T const& x) const
    {
        // valid code for non-integral types,
        // CAN contain code that is invalid for integral types
    }
};

template<class T>
struct numeric_functor<T, true>
{
    T operator()(T const& x) const
    {
        // valid code for integral types
    }
};

template<class T>
T numeric_procedure(T const& x)
{
    return numeric_functor<T, std::is_integral<T>::value>()(x);
}

C'est probablement l'approche la plus flexible si vous souhaitez avoir un contrôle fin et une duplication de code minimale (par exemple, si vous souhaitez également vous spécialiser sur la taille et / ou l'alignement, mais uniquement sur les types à virgule flottante). La correspondance de motif fournie par la spécialisation par modèle partiel est idéale pour ces problèmes avancés. Comme pour le tag-dispatching, les foncteurs auxiliaires sont optimisés par tout compilateur décent.

Le principal inconvénient est la plaque chauffante légèrement plus grande si vous souhaitez seulement vous spécialiser sur une seule condition binaire.

Si constexpr (proposition C ++ 1z)

Il s'agit d'un reboot des propositions précédentes ayant échoué pour static if (qui est utilisé dans le langage de programmation D)

template<class T>
T numeric_procedure(const T& x)
{
    if constexpr (std::is_integral<T>::value) {
        // valid code for integral types
    } else {
        // valid code for non-integral types,
        // CAN contain code that is invalid for integral types
    }
}

Comme avec votre run-time if , tout est à un seul endroit, mais le principal avantage est que la branche else sera entièrement supprimée par le compilateur quand on sait qu'elle n'est pas prise. Un grand avantage est que vous conservez tout le code local, et que vous n’avez pas à utiliser de petites fonctions d’aide comme la répartition des balises ou la spécialisation partielle des modèles.

Concepts-Lite (proposition C ++ 1z)

Concepts-Lite est une spécification technique à venir qui devrait faire partie de la prochaine version majeure de C ++ (C ++ 1z, avec z==7 comme meilleure estimation).

template<Non_integral T>
T numeric_procedure(const T& x)
{
    // valid code for non-integral types,
    // CAN contain code that is invalid for integral types
}    

template<Integral T>
T numeric_procedure(const T& x)
{
    // valid code for integral types
}

Cette approche remplace le mot-clé class ou typename dans les crochets du template< > par un nom de concept décrivant la famille de types pour laquelle le code est censé fonctionner. Cela peut être considéré comme une généralisation des techniques de répartition des étiquettes et de SFINAE. Certains compilateurs (gcc, Clang) ont un support expérimental pour cette fonctionnalité. L'adjectif Lite fait référence à la proposition d'échec de Concepts C ++ 11.


Answer #2

Crédit à @MooingDuck et @Casey

template<class FN1, class FN2, class ...Args>
decltype(auto) if_else_impl(std::true_type, FN1 &&fn1, FN2 &&, Args&&... args)
{
    return fn1(std::forward<Args>(args)...);
}

template<class FN1, class FN2, class ...Args>
decltype(auto) if_else_impl(std::false_type, FN1 &&, FN2 &&fn2, Args&&... args)
{
    return fn2(std::forward<Args>(args)...);
}

#define static_if(...) if_else_impl(__VA_ARGS__, *this)

Et ussage aussi simple que:

static_if(do_it,
    [&](auto& self){ return 1; },
    [&](auto& self){ return self.sum(2); }
);

Fonctionne comme statique si - le compilateur ne va qu'à la branche "true".

PS Vous devez avoir self = *this et faire des appels membres, à cause de bug gcc . Si vous avez des appels lambda imbriqués, vous ne pouvez pas utiliser this-> au lieu de self.


Answer #3

Le compilateur peut être assez intelligent pour voir qu'il peut remplacer le corps de l'instruction if par deux implémentations de fonctions différentes, et il suffit de choisir celle qui convient. Mais à partir de 2014, je doute qu'il existe un compilateur suffisamment intelligent pour le faire. Je peux avoir tort cependant. À la réflexion, std::is_integral est assez simple pour que je pense qu'il sera optimisé.

Votre idée de surcharger le résultat de std::is_integral est une solution possible.

Une autre solution de nettoyage IMHO consiste à utiliser std::enable_if (avec std::is_integral ).





typetraits