Pages

lundi 26 octobre 2015

[C++] Filtrage par type

Exécuter du code dépendant du type des entrées est le principe même de la surcharge. La procédure est plutôt simple pour les types concrets, mais devient rapidement compliquée quand des templates interviennent avec des priorités de concept.

Pour exemple, une fonction foo() spécialisée dans l'ordre pour les types flottants, signés et non-signés requière 3 prototypes de la forme:

enable_if_t<   is_floating_point_v<T> > foo(T);
enable_if_t< ! is_floating_point_v<T> &&   is_signed_v<T> > foo(T);
enable_if_t< ! is_floating_point_v<T> && ! is_signed_v<T> > foo(T);

Comme is_signed inclut également les nombres flottants, il faut les exclure pour empêcher tout appel ambigü, ce qui tend le prototype à grandir inexorablement.

Une première approche consiste à la création d'une hiérarchie de fonctions.

- is_floating_point foo()
- ! is_floating_point foo()
  \- is_signed foo_with_integral()
  \- ! is_signed foo_with_integral()

Ce qui accroît le nombre de fonctions et n'est pas vraiment plus lisible.

Les autres solutions passent par les tags, la spécialisation de template et d'autres que j'oublie peut-être. Au final, la problématique est très proche de l'article précédent concernant static_if.

La solution aussi :D.

Le plus simple consiste en un pattern chaîne de responsabilité appliqué aux protypes de fonction.
Pour rappel, ce pattern permet d'exécuter la première fonction qui répond à une demande. Ici, notre demande sera: cette fonction peut-elle être appelée avec tel type de variable ?

// foo(T xxx)
select(
  xxx,
  if_<std::is_floating_point>([](auto & x) { ... }),
  if_<std::is_unsigned>([](auto & x) { ... }),
  if_<std::is_signed>([](auto & x) { ... }),
  // et plus
  [](std::string const & x) { ... },
  [](auto & x) -> decltype(x.foo()) { ... },
  // ...
  [](auto & x) { static_assert(decltype(void(x),1){},
    "Oups, il n'y a pas de correspondance :/"
  ); }
);


// Implémentation (présentation de la méthode: ici)

template<template<class> class Tpl, class F>
struct case_tpl
{
  F f;

  template<class T>
  auto operator()(T && x, std::enable_if_t<Tpl<std::decay_t<T>>{}>* = 0) const
  { return f(std::forward<T>(x)); }
};

template<template<class> class Tpl, class F>
case_tpl<Tpl, F> if_(F && f) {
  return {f};
}

template<class T>
void select_impl(int, T &) {}

template<class T, class Fn, class... Fns>
auto select_impl(int, T & x, Fn & fn, Fns & ...)
-> decltype(fn(x)) {
  return fn(x);
}

template<class T, class Fn, class... Fns>
auto select_impl(unsigned, T & x, Fn &, Fns & ... fns) {
  return select_impl(1, x, fns...);
}

template<class T, class... Fns>
auto select(T && x, Fns && ... fns) {
  return select_impl(1, x, fns...);
}

On peut même faire une implémentation qui utilise la surcharge et ainsi avoir une garantie forte que seule une fonction existe pour un type donné.

Avec les inconvénients concernant les appels ambigüs ;).

template<class T, class Fn>
auto match(T & x, Fn f) {
  return f(x);
}

template<class T, class Fn1, class Fn2, class... Fns>
auto match(T & x, Fn1 && fn1, Fn2 && fn2, Fns && ... fns) {
  struct F : Fn1, Fn2 {
    using Fn1::operator();
    using Fn2::operator();
    F(Fn1 f1, Fn2 f2) : Fn1(f1), Fn2(f2) {}
  };
  return match(F{fn1,fn2}, fns...)
}

2 commentaires:

Unknown a dit…

As-tu regardé la bibliothèque Boost.Dispatch (qui n'est pas dans Boost pour le moment) ?
http://nt2.metascale.fr/doc/html/the_boost_dispatch_library.html
Le code pourrait être plus clair et tu éviterais le dispatch manuel.

Jonathan Poelen a dit…

J'avais déjà jeté un œil, c'est une approche plutôt "classique" à base de tag que j'aime beaucoup. C'est aussi plus souple et facile à étendre.

D'ailleurs, l'exemple dans les motivations de Boost.Dispatch et le même que dans mon article (comme quoi). Le problème, c'est qu'il ne met pas en valeur l’intérêt premier de la fonction select: prioriser les concepts quand le type est pleinement compatible sur plusieurs niveaux sans hiérarchie apparente.

Si je prends is_printable (pour `stream << x`) et is_stringable (pour `to_string(x)`), les 2 concepts ne sont pas liés, mais selon la situation la préférence ira sur l'un ou l'autre, car les 2 permettent d'afficher quelque chose ou/et d'obtenir un string. Les tags sont moins pratiques ici.

Cela a aussi l'avantage de ne pas casser le flux de lecture.

HS: Si je ne vais pas dans l'interface du blog, je ne vois pas les nouveaux commentaires :/