Recherche…


Introduction

En C ++, la métaprogrammation fait référence à l'utilisation de macros ou de modèles pour générer du code au moment de la compilation.

En général, les macros sont mal vues dans ce rôle et les modèles sont préférés, bien qu'ils ne soient pas aussi génériques.

La métaprogrammation des modèles utilise souvent des calculs à la compilation, que ce soit via des modèles ou des fonctions constexpr , pour atteindre ses objectifs de génération de code. Cependant, les calculs à la compilation ne sont pas des métaprogrammes en soi.

Remarques

La métaprogrammation (ou plus spécifiquement la métaprogrammation des modèles) consiste à utiliser des modèles pour créer des constantes, des fonctions ou des structures de données au moment de la compilation. Cela permet d'effectuer des calculs une fois au moment de la compilation plutôt qu'à chaque exécution.

Calcul des factoriels

Les factoriels peuvent être calculés au moment de la compilation en utilisant des techniques de métaprogrammation de modèles.

#include <iostream>

template<unsigned int n>
struct factorial
{
    enum
    {
        value = n * factorial<n - 1>::value
    };
};

template<>
struct factorial<0>
{
    enum { value = 1 };
};

int main()
{
    std::cout << factorial<7>::value << std::endl;    // prints "5040"
}

factorial est une structure, mais dans la métaprogrammation des modèles, elle est traitée comme une métafonction de modèle. Par convention, les métafonctions de gabarits sont évaluées en vérifiant un membre particulier, soit ::type pour les métafonctions qui génèrent des types, ou ::value pour les métafonctions qui génèrent des valeurs.

Dans le code ci-dessus, nous évaluons la métafonction factorial en instanciant le modèle avec les paramètres que nous voulons passer, et en utilisant ::value pour obtenir le résultat de l'évaluation.

La métafonction elle-même repose sur l'instanciation récursive de la même métafonction avec des valeurs plus petites. La spécialisation factorial<0> représente la condition de terminaison. La métaprogrammation de gabarit a la plupart des restrictions d'un langage de programmation fonctionnel , donc la récursivité est la construction principale de "bouclage".

Comme les métafonctions de modèles s'exécutent au moment de la compilation, leurs résultats peuvent être utilisés dans des contextes nécessitant des valeurs de compilation. Par exemple:

int my_array[factorial<5>::value];

Les tableaux automatiques doivent avoir une taille définie à la compilation. Et le résultat d'une métafonction est une constante à la compilation, il peut donc être utilisé ici.

Limitation : La plupart des compilateurs ne permettent pas une profondeur de récursivité supérieure à une limite. Par exemple, le compilateur g++ limite par défaut la récursion à 256 niveaux. Dans le cas de g++ , le programmeur peut définir la profondeur de récursion en utilisant l' -ftemplate-depth-X .

C ++ 11

Depuis C ++ 11, le modèle std::integral_constant peut être utilisé pour ce type de calcul de modèle:

#include <iostream>
#include <type_traits>

template<long long n>
struct factorial :
  std::integral_constant<long long, n * factorial<n - 1>::value> {};

template<>
struct factorial<0> :
  std::integral_constant<long long, 1> {};

int main()
{
    std::cout << factorial<7>::value << std::endl;    // prints "5040"
}

De plus, les fonctions constexpr deviennent une alternative plus propre.

#include <iostream>

constexpr long long factorial(long long n)
{
  return (n == 0) ? 1 : n * factorial(n - 1);
}

int main()
{
  char test[factorial(3)];
  std::cout << factorial(7) << '\n';
}

Le corps de factorial() est écrit en tant constexpr unique car en C ++ 11, les fonctions constexpr ne peuvent utiliser qu'un sous-ensemble assez limité du langage.

C ++ 14

Depuis C ++ 14, de nombreuses restrictions pour les fonctions constexpr ont été supprimées et elles peuvent maintenant être écrites beaucoup plus facilement:

constexpr long long factorial(long long n)
{
  if (n == 0)
    return 1;
  else
    return n * factorial(n - 1);
}

Ou même:

constexpr long long factorial(int n)
{
  long long result = 1;
  for (int i = 1; i <= n; ++i) {
    result *= i;
  }
  return result;
}
C ++ 17

Depuis c ++ 17, on peut utiliser l'expression des plis pour calculer la factorielle:

#include <iostream>
#include <utility>

template <class T, T N, class I = std::make_integer_sequence<T, N>>
struct factorial;

template <class T, T N, T... Is>
struct factorial<T,N,std::index_sequence<T, Is...>> {
   static constexpr T value = (static_cast<T>(1) * ... * (Is + 1));
};

int main() {
   std::cout << factorial<int, 5>::value << std::endl;
}

Itération sur un paquet de paramètres

Souvent, nous devons effectuer une opération sur chaque élément d'un pack de paramètres de modèle variadic. Il existe plusieurs façons de procéder, et les solutions sont plus faciles à lire et à écrire avec C ++ 17. Supposons que nous voulions simplement imprimer chaque élément d'un pack. La solution la plus simple consiste à faire appel à:

C ++ 11
void print_all(std::ostream& os) {
    // base case
}

template <class T, class... Ts>
void print_all(std::ostream& os, T const& first, Ts const&... rest) {
    os << first;
    
    print_all(os, rest...);
}

Nous pourrions utiliser l'astuce d'expansion pour effectuer tout le streaming en une seule fonction. Cela présente l'avantage de ne pas nécessiter une seconde surcharge, mais présente l'inconvénient d'une lisibilité inférieure aux étoiles:

C ++ 11
template <class... Ts>
void print_all(std::ostream& os, Ts const&... args) {
    using expander = int[];
    (void)expander{0,
        (void(os << args), 0)...
    };
}

Pour une explication de son fonctionnement, voir l'excellente réponse de TC .

C ++ 17

Avec C ++ 17, nous avons deux nouveaux outils puissants dans notre arsenal pour résoudre ce problème. Le premier est une expression de pli:

template <class... Ts>
void print_all(std::ostream& os, Ts const&... args) {
    ((os << args), ...);
}

Et la seconde est if constexpr , ce qui nous permet d’écrire notre solution récursive originale en une seule fonction:

template <class T, class... Ts>
void print_all(std::ostream& os, T const& first, Ts const&... rest) {
    os << first;

    if constexpr (sizeof...(rest) > 0) {        
        // this line will only be instantiated if there are further
        // arguments. if rest... is empty, there will be no call to
        // print_all(os). 
        print_all(os, rest...);
    }
}

Itération avec std :: integer_sequence

Depuis C ++ 14, le standard fournit le modèle de classe

template <class T, T... Ints>
class integer_sequence;

template <std::size_t... Ints>
using index_sequence = std::integer_sequence<std::size_t, Ints...>;

et une métafonction génératrice pour cela:

template <class T, T N>
using make_integer_sequence = std::integer_sequence<T, /* a sequence 0, 1, 2, ..., N-1 */ >;

template<std::size_t N>
using make_index_sequence = make_integer_sequence<std::size_t, N>;

Bien que cela soit standard dans C ++ 14, cela peut être implémenté à l'aide des outils C ++ 11.


Nous pouvons utiliser cet outil pour appeler une fonction avec un std::tuple d'arguments (normalisé en C ++ 17 comme std::apply ):

namespace detail {
    template <class F, class Tuple, std::size_t... Is>
    decltype(auto) apply_impl(F&& f, Tuple&& tpl, std::index_sequence<Is...> ) {
        return std::forward<F>(f)(std::get<Is>(std::forward<Tuple>(tpl))...);
    }
}

template <class F, class Tuple>
decltype(auto) apply(F&& f, Tuple&& tpl) {
    return detail::apply_impl(std::forward<F>(f),
        std::forward<Tuple>(tpl),
        std::make_index_sequence<std::tuple_size<std::decay_t<Tuple>>::value>{});
}


// this will print 3
int f(int, char, double);

auto some_args = std::make_tuple(42, 'x', 3.14);
int r = apply(f, some_args); // calls f(42, 'x', 3.14)

Expédition de tag

Une manière simple de sélectionner des fonctions au moment de la compilation consiste à envoyer une fonction à une paire de fonctions surchargées qui prennent une balise comme un argument (généralement le dernier). Par exemple, pour implémenter std::advance() , nous pouvons envoyer la catégorie itérateur:

namespace details {
    template <class RAIter, class Distance>
    void advance(RAIter& it, Distance n, std::random_access_iterator_tag) {
        it += n;
    }

    template <class BidirIter, class Distance>
    void advance(BidirIter& it, Distance n, std::bidirectional_iterator_tag) {
        if (n > 0) {
            while (n--) ++it;
        }
        else {
            while (n++) --it;
        }
    }

    template <class InputIter, class Distance>
    void advance(InputIter& it, Distance n, std::input_iterator_tag) {
        while (n--) {
            ++it;
        }
    }    
}

template <class Iter, class Distance>
void advance(Iter& it, Distance n) {
    details::advance(it, n, 
            typename std::iterator_traits<Iter>::iterator_category{} );
}

Les arguments std::XY_iterator_tag des fonctions details::advance sont des paramètres de fonction non utilisés. L'implémentation réelle n'a pas d'importance (en fait, elle est complètement vide). Leur seul but est de permettre au compilateur de sélectionner une surcharge en fonction des details::advance classe de balise details::advance est appelée avec.

Dans cet exemple, advance utilise la métafonction iterator_traits<T>::iterator_category qui renvoie une des classes iterator_tag , en fonction du type réel d' Iter . Un objet construit par défaut de la catégorie iterator_category<Iter>::type permet alors au compilateur de sélectionner l'une des différentes surcharges de details::advance . (Ce paramètre de fonction est susceptible d'être complètement optimisé, car il s'agit d'un objet construit par défaut d'une struct vide et jamais utilisé.)

La distribution de balises peut vous donner un code beaucoup plus facile à lire que les équivalents utilisant SFINAE et enable_if .

Remarque: alors que C ++ 17 if constexpr peut simplifier l'implémentation de l' advance en particulier, il ne convient pas pour les implémentations ouvertes contrairement à la distribution de balises.

Détecter si l'expression est valide

Il est possible de détecter si un opérateur ou une fonction peut être appelé sur un type. Pour tester si une classe a une surcharge de std::hash , on peut le faire:

#include <functional> // for std::hash
#include <type_traits> // for std::false_type and std::true_type
#include <utility> // for std::declval

template<class, class = void>
struct has_hash
    : std::false_type
{};

template<class T>
struct has_hash<T, decltype(std::hash<T>()(std::declval<T>()), void())>
    : std::true_type
{};
C ++ 17

Depuis C ++ 17, std::void_t peut être utilisé pour simplifier ce type de construction

#include <functional> // for std::hash
#include <type_traits> // for std::false_type, std::true_type, std::void_t
#include <utility> // for std::declval

template<class, class = std::void_t<> >
struct has_hash
    : std::false_type
{};

template<class T>
struct has_hash<T, std::void_t< decltype(std::hash<T>()(std::declval<T>())) > >
    : std::true_type
{};

std::void_t est défini comme suit:

template< class... > using void_t = void;

Pour détecter si un opérateur, tel que operator< est défini, la syntaxe est presque la même:

template<class, class = void>
struct has_less_than
    : std::false_type
{};

template<class T>
struct has_less_than<T, decltype(std::declval<T>() < std::declval<T>(), void())>
    : std::true_type
{};

Ceux-ci peuvent être utilisés pour utiliser un std::unordered_map<T> si T a une surcharge pour std::hash , mais tentez sinon d'utiliser un std::map<T> :

template <class K, class V>
using hash_invariant_map = std::conditional_t<
    has_hash<K>::value,
    std::unordered_map<K, V>,
    std::map<K,V>>;    

Calcul de la puissance avec C ++ 11 (et supérieur)

Avec C ++ 11 et des calculs plus élevés au moment de la compilation peuvent être beaucoup plus faciles. Par exemple, le calcul de la puissance d'un nombre donné au moment de la compilation sera le suivant:

template <typename T>
constexpr T calculatePower(T value, unsigned power) {
    return power == 0 ? 1 : value * calculatePower(value, power-1);
}

Le mot-clé constexpr est responsable du calcul de la fonction au moment de la compilation, et alors seulement, lorsque toutes les conditions requises seront satisfaites (voir plus loin le mot-clé constexpr), tous les arguments doivent être connus au moment de la compilation.

Remarque: En C ++ 11, la fonction constexpr ne doit composer qu'à partir d'une seule déclaration de retour.

Avantages: En comparant cela à la méthode standard de calcul de la compilation, cette méthode est également utile pour les calculs d’exécution. Cela signifie que si les arguments de la fonction ne sont pas connus au moment de la compilation (par exemple la valeur et la puissance sont données en entrée via l'utilisateur), alors la fonction est exécutée dans un temps de compilation, il n'est donc pas nécessaire de dupliquer un code serait forcé dans les anciennes normes de C ++).

Par exemple

void useExample() {
    constexpr int compileTimeCalculated = calculatePower(3, 3); // computes at compile time,
                               // as both arguments are known at compilation time
                               // and used for a constant expression.
    int value;
    std::cin >> value;
    int runtimeCalculated = calculatePower(value, 3);  // runtime calculated,
                                    // because value is known only at runtime.
}
C ++ 17

Une autre façon de calculer la puissance au moment de la compilation peut utiliser l'expression de repli comme suit:

#include <iostream>
#include <utility>

template <class T, T V, T N, class I = std::make_integer_sequence<T, N>>
struct power;

template <class T, T V, T N, T... Is>
struct power<T, V, N, std::integer_sequence<T, Is...>> {
   static constexpr T value = (static_cast<T>(1) * ... * (V * static_cast<bool>(Is + 1)));
};

int main() {
   std::cout << power<int, 4, 2>::value << std::endl;
}

Distinction manuelle des types avec n'importe quel type T

Lors de l'implémentation de SFINAE à l' aide de std::enable_if , il est souvent utile d'avoir accès à des modèles d'aide qui déterminent si un type donné T correspond à un ensemble de critères.

Pour nous aider, le standard fournit déjà deux types analogiques à true et false qui sont std::true_type et std::false_type .

L'exemple suivant montre comment détecter si un type T est un pointeur ou non, le modèle is_pointer imite le comportement de l'assistant std::is_pointer standard:

template <typename T>
struct is_pointer_: std::false_type {};

template <typename T>
struct is_pointer_<T*>: std::true_type {};

template <typename T>
struct is_pointer: is_pointer_<typename std::remove_cv<T>::type> { }

Le code ci-dessus comporte trois étapes (il suffit parfois de deux):

  1. La première déclaration de is_pointer_ est le cas par défaut et hérite de std::false_type . Le cas par défaut devrait toujours hériter de std::false_type car il est analogue à une " false condition".

  2. La seconde déclaration spécialise le modèle is_pointer_ pour le pointeur T* sans se soucier de ce qu'est réellement T Cette version hérite de std::true_type .

  3. La troisième déclaration (la vraie) supprime simplement toutes les informations inutiles de T (dans ce cas, nous const volatile qualificateurs const et volatile ), puis nous nous basons sur l'une des deux déclarations précédentes.

Puisque is_pointer<T> est une classe, pour accéder à sa valeur, vous devez soit:

  • Use ::value , par exemple is_pointer<int>::value - value est un membre de classe statique de type bool hérité de std::true_type ou std::false_type ;
  • Construire un objet de ce type, par exemple is_pointer<int>{} - Cela fonctionne car std::is_pointer hérite de son constructeur par défaut de std::true_type ou std::false_type (qui ont des constructeurs constexpr ) et std::true_type et std::false_type a des opérateurs de conversion constexpr à bool .

C'est une bonne habitude de fournir des "modèles d'assistance" qui vous permettent d'accéder directement à la valeur:

template <typename T>
constexpr bool is_pointer_v = is_pointer<T>::value;
C ++ 17

En C ++ 17 et versions _v , la plupart des modèles d'aide fournissent déjà une version _v , par exemple:

template< class T > constexpr bool is_pointer_v = is_pointer<T>::value;
template< class T > constexpr bool is_reference_v = is_reference<T>::value;

If-then-else

C ++ 11

Le type std::conditional dans l'en-tête de bibliothèque standard <type_traits> peut sélectionner un type ou l'autre, en fonction d'une valeur booléenne à la compilation:

template<typename T>
struct ValueOrPointer
{
    typename std::conditional<(sizeof(T) > sizeof(void*)), T*, T>::type vop;
};

Cette structure contient un pointeur sur T si T est plus grand que la taille d'un pointeur ou T si elle est plus petite ou égale à la taille d'un pointeur. Par conséquent, sizeof(ValueOrPointer) sera toujours <= sizeof(void*) .

Min / Max générique avec nombre d'arguments variable

C ++ 11

Il est possible d'écrire une fonction générique (par exemple min ) qui accepte différents types numériques et un nombre d'arguments arbitraire par méta-programmation de modèles. Cette fonction déclare un min pour deux arguments et récursivement pour plus.

template <typename T1, typename T2>
auto min(const T1 &a, const T2 &b) 
-> typename std::common_type<const T1&, const T2&>::type
{
    return a < b ? a : b;
}

template <typename T1, typename T2, typename ... Args>
auto min(const T1 &a, const T2 &b, const Args& ... args)
-> typename std::common_type<const T1&, const T2&, const Args& ...>::type
{
    return min(min(a, b), args...);
}

auto minimum = min(4, 5.8f, 3, 1.8, 3, 1.1, 9);


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow