Il y a quelques jours, j’ai découvert les fold expressions, une nouveauté de C++17. J’ai trouvé ça cool. Je me suis surtout demandé à quoi ça pourrait me servir. Et puis j’ai pensé à un code écrit il y a quelques semaines et et je me suis dit que ça aurait sans doute été plus simple avec cette fonctionnalité. Je ne vais pas modifier le vrai code puisqu’il marche et est déjà utilisé dans un logiciel professionnel mais j’ai fait un code d’essai pour voir. Et effectivement, c’est bien mieux ! Je vais vous montrer ça.
Le but est de monitorer des variables pendant l’exécution du programme. J’ai pour cela une classe Monitor à laquelle on peut ajouter des variables à surveiller. Voici à quoi ressemble le code :
#include <iostream>
class Monitor {
public:
void watch(short& s [[maybe_unused]]) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void watch(int& i [[maybe_unused]]) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void watch(long& l [[maybe_unused]]) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
};
int main(void) {
Monitor m;
short s = 0;
m.watch(s);
int i = 0;
m.watch(i);
long l = 42;
m.watch(l);
}
J’en profite au passage pour vous montrer l’extensions GNU __PRETTY_FUNCTION__ qui permet d’afficher joliment le nom de la fonction et une utilisation de l’attribut maybe_unused (une autre nouveauté de C++17) pour à éviter le classique (void) paramètre_qui_ne_sert_pas dans le corps des fonctions. On a ainsi un code qui compile sans warning et qui produit la sortie suivante :
void Monitor::watch(short int&)
void Monitor::watch(int&)
void Monitor::watch(long int&)
À l’utilisation, il s’avère qu’il est pratique de pouvoir ajouter plusieurs variables d’un coup, par exemple en écrivant m.watch(s, i, l);. L’idée est de créer une fonction template prenant un nombre variable de paramètres, chacun pouvant être d’un type différent des autres. C++11 a apporté cette possibilité, grâce aux variadic templates et aux parameter packs. Une telle fonction ressemblerait à ça :
template<typename...Targs>
void function(Targs... args) {
}
Ça se corse quand on souhaite itérer sur les éléments du pack… La technique classique consiste à fait faire des appels récursifs à la fonction pour réduire le pack. Pour la classe Monitor, il convient donc de rajouter deux fonctions :
class Monitor {
public:
// idem
template<typename T, typename...Targs>
void watchSeveral_recursive(T& t, Targs&...args) {
std::cout << __PRETTY_FUNCTION__ << std::endl;
watch(t);
watchSeveral_recursive(args...);
}
private:
template<typename T>
void watchSeveral_recursive(T& t) {
std::cout << __PRETTY_FUNCTION__ << " (final) " << std::endl;
watch(t);
}
};
La fonction publique est effectivement récursive. L’astuce est d’avoir un paramètre simple en plus du pack. Chaque appel imbriqué va ainsi « réduire le pack », puisque le premier élément du pack va devenir le premier argument et que le reste du pack va créer un nouveau pack en deuxième argument. Comme toute récursion, il faut un cas d’arrêt : c’est le rôle de la fonction privée. Elle est appelée quand le pack ne contient plus qu’un élément puisque le compilateur va préférer cette surcharge privée à la surcharge publique dans un tel cas. Ça donne ça à l’utilisation :
int main(void) {
Monitor m;
short s = 0;
int i = 0;
long l = 42;
m.watchSeveral_recursive(s, i, l);
}
void Monitor::watchSeveral_recursive(T&, Targs& ...)
[with T = short int; Targs = {int, long int}]
void Monitor::watch(short int&)
void Monitor::watchSeveral_recursive(T&, Targs& ...)
[with T = int; Targs = {long int}]
void Monitor::watch(int&)
void Monitor::watchSeveral_recursive(T&)
[with T = long int] (final)
void Monitor::watch(long int&)
Sympa, hein ?
Voici maintenant la version avec une fold expression :
class Monitor {
public:
// idem
template<typename...Ts>
void watchSeveral_withFoldExpr(Ts&...args) {
std::cout <<__PRETTY_FUNCTION__ << std::endl;
(watch(args), ...);
// Notez l'utilisation de parentheses !
}
};
Elle s’utilise évidemment de la même manière que précédemment :
int main(void) {
Monitor m;
short s = 0;
int i = 0;
long l = 42;
m.watchSeveral_withFoldExpr(s, i, l);
}
La sortie est bien sûr ce qu’on s’attend à obtenir :
void Monitor::watchSeveral_withFoldExpr(Ts& ...)
[with Ts = {short int, int, long int}]
void Monitor::watch(short int&)
void Monitor::watch(int&)
void Monitor::watch(long int&)
Voilà, voilà… J’avais dit que c’était mieux ^^ C’est tellement simple que je n’ai pas besoin de vous expliquer le code. Il suffit que de savoir que c’est une fold expression pour le comprendre. C’est une fonctionnalité toute nouvelle, peu de gens la connaissent, mais vous oui après la lecture de cet article 🙂