Pages

lundi 21 septembre 2015

[c++] static_if

Le but de `static_if` est de ne pas vérifier la validité d'un code dans la phase de compilation. Le code doit être syntaxiquement valide, mais n'a pas l'obligation de pouvoir être compilé.

Plutôt étrange n'est-ce pas ? Cette propriété se révèle pourtant fort pratique dans les fonctions templates. Prenons comme exemple relativement bidon une fonction `foo(T & array)` qui vérifie si `array.size()` est au moins à 4 et, dans le cas contraire, agrandit le container avec `array.resize(4)`.

// pour T[N]
if (std::is_array<T>{})) {
  static_assert(std::extent<T>{} >= 4, "");
}
// pour std::vector
else { 
 array.resize(4);
}

Le problème ici: `static_assert` est toujours exécuté et `resize()` toujours évalué. Résultat, le code ne compile jamais, car `resize()` n'existe pas pour les tableaux et `size()` n'est pas `constexpr` pour `std::vector`.

Évidemment, `static_if` résout le problème. Dit comme cela, on pourrait croire que c'est un nouveau mot clef du langage, mais non. (Il eut une proposition dans ce sens, mais celle-ci fut rejetée.) Alors comment faire ?

Alternatives au static_if

En réalité, il existe plusieurs alternatives utilisées, et ce, bien avant C++11.

Trait et tag

Parmi celles qui font le sujet d'un de mes articles: les 'tags', souvent associés à un type_trait pour l'extraction.

Sur le principe, un type catégorie est extrait d'un paramètre et propagé à des fonctions surchargées sur la catégorie désirée. Malheureusement, cette approche disperse le code et rend la lecture difficile pour des besoins spécifiques et ponctuels. L'autre inconvénient est la transmission du scope de variable qui s'avère délicat (c'est une union (dans le sens mathématique) de toutes les variables utilisées par l'ensemble des surcharges).

Spécialisation de template

Sans plus m'étendre sur cette technique, le principe est exactement le même que pour les tags, sauf que l'on remplace la surcharge de fonction par de la spécialisation de template.

Classes internes sélectionnées avec std::conditional

La classe interne (comme utilisée dans l'article précédent) permet un code monolithique, mais n'a pas de conséquence sur le scope de variable, il faut toujours le propager. Sa lourdeur d'écriture est aussi un peu rédhibitoire.

Lambda

Les lambdas permettent de capturer le scope et se définissent n'importe où. Mais un problème survient, comment empêcher l'évaluation de la lambda si la condition n'est pas respectée ?

Depuis C++14, il existe les lambdas polymorphes. Celles-ci permettent l'usage de template à travers un type `auto`. Comme les templates ne sont évaluées qu'à leur appel, on peut faire du branchement conditionnel à la compilation en utilisant de la surcharge sur `std::true_type` et `std::false_type`. Au final, static_if devient un appel de fonction.

#include <type_traits>

class dummy { dummy(){} void operator()() const {} };

template<class TrueFn, class FalseFn = dummy>
auto static_if(std::true_type, TrueFn yes_fn, FalseFn const & = FalseFn{}) {
  return yes_fn(std::true_type{});
}

template<class TrueFn, class FalseFn>
auto static_if(std::false_type, TrueFn const &, FalseFn no_fn) {
  return no_fn(std::false_type{});
}


template<class T>
void foo(T & array) {
  static_if(std::is_array<T>{}, [&](auto){
    static_assert(std::extent<T>{} >= 4, "");
  },
  /*else*/[&](auto){
    array.resize(4);
  });
}


#include <vector>

int main()
{
  {
    std::vector<int> a;
    foo(a); // ok
  }
  {
    int a[5];
    foo(a); // ok
  }
  {
    int a[2];
    foo(a); // static_assert
  }
}

J'ai mis une valeur arbitraire pour le paramètre envoyé aux lambdas, on pourrait aussi le rendre optionnel en utilisant la méthode présentée dans cet article.

L'implémentation de static_if ne se prête pas très bien à l'imbrication. Une autre manière de faire, passe par une classe et du chaînage `static_if(...).else_if(...)`. C'est un peu plus compliqué si on veut un type de retour et on ne pourra pas utiliser de AAA sans appeler explicitement une fonction `eval()`.

auto xxx = static_if(..., []{ return 1; }).else_([]{ return 2; });
// decltype(xxx) != int (définit par l'implémentation de static_if)
auto xxx = static_if(..., []{ return 1; }).else_([]{ return 2; }).eval();
// decltype(xxx) = int

Je pense qu'il vaut alors mieux se tourner vers une forme proche du switch.

static_switch(xxx
, case_<std::is_array>() = [&](auto){ static_assert(std::extent<T>{} >= 4, ""); }
, case_<yyy>() | case_<zzz>() = [&]{ ... }
, default_ = []{ ... }
);

Que je laisse à la discrétion de chacun ;)

Aucun commentaire: