Méta-fonction et continuation

Article suivant: Presque toujours std::move
Article précédent: Simuler une vtable sans fonction virtuelle

J’ai pas mal bossé avec des bibliothèques de méta-programmation, et une que j’apprécie particulièrement est Kvasir.Mpl. Son originalité réside dans le support des continuations et –parce que cela va bien de paire,– l’évaluation paresseuse ainsi que des algorithmes pensés pour manipuler des packs (template variadique) plutôt que des listes de type (list<Ts...>).

Continuation

Chaque algorithme dispose d’un paramètre qui décrit l’étape suivante du traitement, c’est la continuation. En shell ou dans des bibliothèques comme rangev3, les continuations se font avec l’opérateur |. Par exemple grep x | wc -l ou filter(is_odd) | take(6). Il n’est pas possible d’utiliser | avec des templates, alors la continuation est un paramètre ajouté à l’algorithme. En chaînant les continuations, on construit un nouvel algorithme qui s’utilise avec call<algo, args...>.

// `C` pour continuation

template<class C>
struct add_const;

template<class C>
struct add_lvalue_reference;

template<class C>
using add_const_lvalue_reference = add_const<add_lvalue_reference<C>>; // un chaînage

Les continuations limitent drastiquement le recours aux “lambdas”. Dans d’autres bibliothèques du genre, add_const_lvalue_reference s’écrirait lambda<add_lvalue_reference, lambda<add_const, _>> ce qui est, il faut l’avouer, beaucoup moins glamour.

Dans des algorithmes du type transformation, l’impact sur la lecture du code est immédiat:

transform<lambda<next, lambda<times, _1, _2>>, L1, L2>
// vs
call<transform<times<next<>/*, C=listify*/>, L1, L2>

Qui se traduit par la construction d’une nouvelle liste avec la formule (x*y)+1.

Ce code met bien en évidence un autre aspect des continuations: la lecture du code se fait de gauche à droite alors que les lambdas se lisent de droite à gauche et oblige le lecteur à faire des allers/retours pour suivre le flux de code.

transform<lambda<next, lambda<times, _1, _2>>, L1, L2>
 /* 1 */       /* 3 */       /* 2 */
call<transform<times<next<>/*, C=listify*/>, L1, L2>
      /* 1 *//* 2 *//* 3 */      /* 4 */

Méta-fonction

Au niveau de kvasir, une méta-fonction est un type qui possède un membre template nommé f. Tous les algorithmes de kvasir sont des méta-fonctions et l’appel se fait avec call<metafunc, param1, param2,...>. Si on complète add_const et add_lvalue_reference plus haut, cela donne:

// Cette implémentation de call est volontairement simplifiée
// et ne fonctionne pas pour tous F
template<class F, class... Ts>
using call = typename F::template f<Ts...>;

template<class C>
struct add_const
{
  template<class T>
  using f = call<C, T const>;
};

template<class C>
struct add_lvalue_reference
{
  template<class T>
  using f = call<C, T&>;
};

Il ne reste qu’une dernière étape, la continuation finale lorsqu’il n’y a plus rien à faire. Les plus utilisés sont au nombre de 2 et servent de valeur par défaut à l’ensemble des algorithmes, j’ai nommé identity et listify.

struct identity
{
  template<class T>
  using f = T;
};

template<class...>
class list {};

struct listify
{
  template<class... Ts>
  using f = list<Ts...>;
};

C’est beau, c’est propre

Il y a quelques jours, après mon petit passage quotidien dans libstdc++ et en regardant std::decay, je me suis dit c’est beau, c’est propre qu’il faudrait comparer avec une implémentation qui utilise les concepts de kvasir.

Voici en l’état la version fournie avec gcc-8.2:

// Decay trait for arrays and functions, used for perfect forwarding
// in make_pair, make_tuple, etc.
template<typename _Up,
     bool _IsArray = is_array<_Up>::value,
     bool _IsFunction = is_function<_Up>::value>
  struct __decay_selector;

// NB: DR 705.
template<typename _Up>
  struct __decay_selector<_Up, false, false>
  { typedef typename remove_cv<_Up>::type __type; };

template<typename _Up>
  struct __decay_selector<_Up, true, false>
  { typedef typename remove_extent<_Up>::type* __type; };

template<typename _Up>
  struct __decay_selector<_Up, false, true>
  { typedef typename add_pointer<_Up>::type __type; };

/// decay
template<typename _Tp>
  class decay
  {
    typedef typename remove_reference<_Tp>::type __remove_type;

  public:
    typedef typename __decay_selector<__remove_type>::__type type;
  };

En réalité, ce qui m’a particulièrement tiqué se situe dans la déclaration de __decay_selector: is_array et is_function sont toujours évaluées avec _Up et il faut écrire plusieurs spécialisations.

L’idée du sélecteur est un bon concept et se remplace facilement par une suite de conditions:

// ce que fait plus ou moins libc++ de llvm
template<class T>
using __decay_selector = conditional<
  is_array_v<T>,
  remove_extent_t<T>*,
  conditional_t<
    is_function_v<T>,
    add_pointer_t<T>,
    remove_cv_t<T>
  >
>;

Qui engendre son lot de problèmes: en plus des 2 traits précédents, remove_extent, add_pointer et remove_cv sont toujours déclarés avec T. Cela ne pose pas de réel problème ici – à part le temps de compilation–, malheureusement, dans certaines situations, les branches ne doivent s’évaluer qu’en fonction du prédicat et les traits de la STL ne le permettent pas sans ajouter des intermédiaires.

Mais les continuations changent tout puisque les méta-fonctions ne sont évaluées que pour la branche qui respecte le prédicat. Ainsi, remove_extent<T> ne sera pas instancié lorsque is_array_v<T> est faux, ce qui rend les continuations très facilement composables.

template<class T>
struct decay
{
  using type = call<
    if_<
      cfl<std::is_array>,
      cfe<std::remove_extent, cfe<detail::add_pointer_impl>>,
      if_<
        cfl<std::is_function>,
        cfe<std::add_pointer>,
        cfe<std::remove_cv>
      >
    >,
    std::remove_reference_t<T>
  >;
};

Avec l’implémentation de if_, cfl (continuation from a lazy metafunction) et cfe (continuation from a eager metafunction):

namespace detail
{
  template<bool B>
  struct conditional;
}

template<class P, class TC, class FC>
struct if_
{
  template<class... Ts>
  using f = call<call<detail::conditional<call<P, Ts...>::value>, TC, FC>, Ts...>;
};

template<template<class...> class F, class C = identity>
struct cfl
{
  template<class... Ts>
  using f = call<C, F<Ts...>>;
};

template<template<class...> class F, class C = identity>
struct cfe
{
  template<class... Ts>
  using f = call<C, typename F<Ts...>::type>;
};


namespace detail
{
  template<bool B>
  struct conditional
  {
    template<class T, class U> using f = T;
  };

  template<>
  struct conditional<false>
  {
    template<class T, class U> using f = U;
  };
}

L’ensemble des sources se trouve sur github .

Commentaires

Aucun commentaire pour le moment :'(

Le système de commentaire passe par les issues de github et aucun n'est associée au billet. Vous pouvez faire votre commentaire dans une issue qui a comme titre celui du billet. Je me chargerai de les associer.

Revenir en haut