C++
Métaprogrammation
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
.
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.
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;
}
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 à:
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:
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 .
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
{};
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
{};
où 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.
}
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):
La première déclaration de
is_pointer_
est le cas par défaut et hérite destd::false_type
. Le cas par défaut devrait toujours hériter destd::false_type
car il est analogue à une "false
condition".La seconde déclaration spécialise le modèle
is_pointer_
pour le pointeurT*
sans se soucier de ce qu'est réellementT
Cette version hérite destd::true_type
.La troisième déclaration (la vraie) supprime simplement toutes les informations inutiles de
T
(dans ce cas, nousconst
volatile
qualificateursconst
etvolatile
), 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 exempleis_pointer<int>::value
-value
est un membre de classe statique de typebool
hérité destd::true_type
oustd::false_type
; - Construire un objet de ce type, par exemple
is_pointer<int>{}
- Cela fonctionne carstd::is_pointer
hérite de son constructeur par défaut destd::true_type
oustd::false_type
(qui ont des constructeursconstexpr
) etstd::true_type
etstd::false_type
a des opérateurs de conversionconstexpr
à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;
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
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
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);