C++ et les Fold Expressions

Avant de lire ce texte, il serait sans doute préférable de vous familiariser au préalable avec les templates variadiques.

Notez que le texte est embryonnaire; j'enrichirai le tout quand j'aurai pu expérimenter moi-même avec ce mécanisme.

Avec C++ 17, C++ offre un nouveau (pour ce langage) mécanisme de manipulation d'expressions variadiques nommé Fold Expressions, par lequel le compilateur gagne une « compréhension » de l'expression en extension de certains opérateurs sur un Parameter Pack.

Par exemple, soit la fonction variadique moyenne() ci-dessous, légale avec C++ 14 :

template <class T>
   auto somme_elements(T && val) {
      return val;
   }
template <class T, class ... Args>
   auto somme_elements(T && val, Args &&... args) {
      return val + somme_elements(std::forward<Args>(args)...);
   }

template <class R, class ... Args>
   R moyenne(Args &&... args) {
      return static_cast<R>(somme_elements(std::forward<Args>(args)...)) / sizeof...(Args);
   }

Sans que cette fonction ne soit très complexe, elle demande une forme de pensée récursive et une construction manuelle de la somme d'une séquence variadique de valeurs, et demande de comprendre le relais parfait pour relayer les paramètres correctement à chaque appel de la chaîne. Comparez avec la fonction C++ 17 au comportement équivalent :

template <class ... Args>
   auto somme_elements(Args &&... args) {
      return args + ...;
   }

template <class R, class ... Args>
   R moyenne(Args &&... args) {
      return static_cast<R>(somme_elements(std::forward<Args>(args)...)) / sizeof...(Args);
    }

... ou encore, plus simplement (présumant que somme_elements() ne soit pas nécessaire pour d'autres fins) :

template <class R, class ... Args>
   R moyenne(Args &&... args) {
      return static_cast<R>(args + ...) / sizeof...(Args);
   }

... ce qui est remarquablement plus simple.

Les formats possibles sont :

Nom Expression pour un opérateur donné Interprétation Exemple (naïf)

Unary Right Fold

pack ...

template <class ... Ts>
   auto somme(Ts ... args) { return args + ...; }

Unary Left Fold

... pack

template <class ... Ts>
   auto somme(Ts ... args) { return ... + args; }

Binary Right Fold

pack ... init

template <class ... Ts>
   auto somme(Ts ... args) { return args + ... + 0; }

Binary Left Fold

init ... pack

template <class ... Ts>
   auto somme(Ts ... args) { return 0 + ... + args; }

Les Fold Expressions s'appliquent à la plupart des opérateurs, ce qui permet par exemple :

... d'appeler plusieurs fois la même fonction avec les éléments d'une séquence variadique (remarquez ici que nous exploitons l'opérateur ,)... 

// ...
template <class T, class ... Ts>
   auto ajout_un_a_un(Ts &&... elems) {
      vector<T> v;
      (v.push_back(elems), ...); // <--
      return v;
   }

... d'afficher plusieurs valeurs en séquence (sans espace entre eux dans l'exemple à droite), en utilisant ici l'opérateur << ...

template <class ... Args>
   ostream& projeter(ostream &os, Args &&... args) {
      return (os << ... << args) << endl;
   }

... de réaliser une conjonction logique de plusieurs prédicats, à l'aide de l'opérateur &&...

template <class T, class ... Pred>
   bool conjonction(T &&obj, Pred &&... preds) {
      return preds(obj) && ... && true;
   }

... de vérifier si une valeur est équivalente (au sens de l'opérateur ==) à au moins une d'un groupe arbitrairement grand d'autres valeurs...

template <class T, class ... Ts>
   constexpr bool any_one_of(T && one, Ts &&... others) {
      return ((one == others) || ...);
   }

... générer une série de λ s'appliquant successivement à chaque élément d'une séquence variadique, etc.

template<class... T>
   void afficher_un_par_ligne(T... a) {
      ([&](auto &&ai) { cout << ai << endl; }(a),...);
   }

Un nouveau terrain de jeu pour les programmeuses et les programmeurs qui apprécient C++, en somme.

Créer un masque

Exemple sympathique, emprunté à Martin Moene (source) : générer un masque (un bitmask) à l'aide d'une Fold Expression. Aussi simple que ceci :

template<class T, class ...Args>
   constexpr T bitmask(Args&&... args) {
      return static_cast<T>( ((1 << args) | ...) );
   }

Voici un exemple complet générant un masque de valeur 7 (bits 0, 1 et 2 allumés seulement; notez que l'ordre n'est pas important) sur un unsigned int en profitant de cette chic fonction :

#include <iostream>
#include <iomanip>
#include <type_traits>
using namespace std;
template <class T>
   void print_bin(ostream &os, T && arg, int stride = 4) {
      static_assert(is_integral_v<decay_t<T>>);
      constexpr auto bits_per_byte = numeric_limits<unsigned char>::digits;
      constexpr auto nbits = sizeof arg * bits_per_byte;
      for(int i = nbits; --i >= 0;) {
         os << ((arg & (1 << i)) != 0? 1 : 0);
         if (i && i % stride == 0) os << '\'';
      }
   }
template<class T, class ...Args>
   constexpr T bitmask(Args&&... args) {
      return static_cast<T>( ((1 << args) | ...) );
   }
int main() {
   constexpr auto testmask = bitmask<unsigned int>(0, 1, 2);
   cout << setw(sizeof(unsigned int)) << setfill('0') << hex << testmask << dec << endl;
   print_bin(cout, testmask);
   cout << endl;
}

... ce qui affichera bien entendu :

0007
0000'0000'0000'0000'0000'0000'0000'0111

C'est plutôt sympathique, n'est-ce pas?

Lectures complémentaires

Quelques liens pour enrichir le propos.

Il existe des applications insoupçonnées aux Fold Expressions. Par exemple, ce qui suit :

struct Z { int h; };
struct Y { Z g; };
struct X { Y f; };
template <class T, class ...Args>
   int go_deeper(T && x, Args... args) {
      return (x .* ... .* args);
   }
int main() {
   X x { { { 5 } } };
   return go_deeper(x, &X::f, &Y::g, &Z::h);
}

... retournera 5, ce qui porte à réfléchir (voir https://wandbox.org/permlink/vJSOflbmflSBmQht pour une démonstration).


Valid XHTML 1.0 Transitional

CSS Valide !