Au cœur d'un variant

Article suivant: Comparaison de différentes implémentations de mp_index_of
Article précédent: Faites parler votre compilateur

Cet article va être consacré à la réalisation d’une classe variant comme on peut la trouver dans la STL, boost et autres. Il existe de nombreuses techniques plus ou moins simples à réaliser et plus ou moins coûteuses à l’exécution. Je vais faire un petit tour de ce que j’ai pu voir et comment les implémenter.

Rappel sur ce qu’est un variant

Un variant est une union sécurisée comme on peut le trouver dans les langages fonctionnels. Contrairement aux union classique du C++, le variant garde l’information du type manipulé. C’est en cela qu’il est sécurisé, on ne sélectionne pas une valeur d’un certain type, mais on fournit une fonction que le variant appelle avec le type enregistré.

Même si le variant peut aisément remplacer l’héritage lorsque le nombre de classe dérivée est connue, il est beaucoup plus adapté lorsque les valeurs priment sur les comportements. Par exemple, une valeur d’une structure JSON représente un nombre, un tableau, une chaîne de caractères ou un objet. Il n’y a pas de comportement commun, les traitements se feront en fonction du type de la valeur.

Un premier variant

Notre version minimale de variant va contenir:

  • Les constructeurs par défaut, de copie et de déplacement.
  • Un constructeur pour initialiser avec un type de la liste.
  • Les opérateurs d’affectation correspondant aux constructeurs.
  • Une fonction visit (en membre, pour des raisons de simplification).

Info :

L’implémentation qui va suivre se veut simple et surtout naïve. De ce fait, elle est totalement inefficace et montre l’exemple à ne pas suivre. Elle servira néanmoins de base de travail et sera peaufinée tout au long des chapitres pour atteindre l’idéal du variant.

Comme le type change en cours de route, nous allons utiliser en interne une classe de base qui pour chaque dérivée va contenir le type réel. Grâce à cela, le type stocké pourra être supprimé et un nouveau type pourra y être enregistré. Cette classe de base pourra aussi servir à implémenter l’opérateur de copie via une fonction clone.

namespace detail
{
  struct VariantBase
  {
    virtual std::unique_ptr<VariantBase> clone() = 0;
    virtual ~VariantBase() = default;
  };
}

template<class... Ts>
class Variant
{
public:
  Variant() = default;
  Variant(Variant&&) = default;
  Variant(Variant const&);

  template<class T>
  Variant(T&& x);

  Variant& operator=(Variant&&) = default;
  Variant& operator=(Variant const&);

  template<class T>
  Variant& operator=(T&& x);

  template<class F>
  auto visit(F&&);

private:
  std::unique_ptr<detail::VariantBase> impl_;
};

Pour ne pas parasiter les codes, je n’ajoute pas les noexcept. De toute façon, avec les allocations dynamiques, cela ne va pas être évident.

Toute la difficulté va se trouver dans les implémentations de VariantBase et la fonction visit. Pour en faire une dérivée, un pattern assez commun va être utilisé, celui d’avoir une classe VariantImpl template sur le type à stocker.

namespace detail
{
  template<class T>
  struct VariantImpl : VariantBase
  {
    template<class U>
    VariantImpl(U&& x)
    : value_(std::forward<U>(x))
    {}

    std::unique_ptr<VariantBase> clone() override
    {
      return std::make_unique<VariantImpl>(value_);
    }

    T value_;
  };

  template<class T>
  auto make_variant_impl(T&& x)
  {
    return std::make_unique<VariantImpl<std::decay_t<T>>>(
      std::forward<T>(x));
  }
}

template<class... Ts>
Variant<Ts...>::Variant(Variant const& other)
: impl_(other.impl_->clone())
{}

template<class... Ts>
template<class T>
Variant<Ts...>::Variant(T&& x)
: impl_(detail::make_variant_impl(std::forward<T>(x)))
{}

template<class... Ts>
Variant<Ts...>& Variant<Ts...>::operator=(Variant const& other)
{
  impl_ = other.impl_ ? other.impl_->clone() : nullptr;
  return *this;
}

template<class... Ts>
template<class T>
Variant<Ts...>& Variant<Ts...>::operator=(T&& x)
{
  impl_ = detail::make_variant_impl(std::forward<T>(x));
  return *this;
}

En réalité ce qu’on vient de faire ici n’est ni plus ni moins qu’un std::any. Si on réfléchit bien, nous ne sommes pas limités dans les types à stocker et il n’y a aucune vérification au niveau de l’initialisation d’une valeur. C’est mal, mais on va rester comme cela pour le moment.

Reste ensuite la fonction visit. À ce stade, je dirais que la solution la plus naturelle est d’utiliser dynamic_cast pour déterminer le type réel et appeler la bonne surcharge de fonction.

template<class... Ts>
template<class F>
auto Variant<Ts...>::visit(F&& f)
{
  assert(impl_);
  auto visit_impl = [&](auto rec, auto* t, auto*... ts){
    using Impl = detail::VariantImpl<std::decay_t<decltype(*t)>>;
    if constexpr (sizeof...(ts)) {
      auto* impl = dynamic_cast<Impl*>(impl_.get());
      return impl ? f(impl->value_) : rec(rec, ts...);
    }
    else {
      (void)rec;
      return f(static_cast<Impl*>(impl_.get())->value_);
    }
  };
  return visit_impl(visit_impl, static_cast<Ts*>(nullptr)...);
}

Cette implémentation parcourt récursivement les types du variant pour trouver celui qui correspond à la valeur de impl_, appel f avec le bon type puis propage son retour en remontant la pile d’appel. Le dernier élément est un cas spécial traité dans le else car, quand on le compare avec dynamic_cast, le résultat est toujours vrai. Comme notre variant ne contient –normalement– qu’un nombre restreint de types, si la valeur de impl_ ne correspond pas aux types qui précèdent le dernier, alors impl_ est forcément du type du dernier élément.

Moi j’aime pas dynamic_cast

dynamic_cast est souvent un signe révélateur d’un problème de conception. Si on abstrait les valeurs, c’est dans le but de ne pas se soucier du type de l’implémentation. Or, un variant met le focus sur le type et rend caduque cette abstraction. Seulement, dynamic_cast a un coût d’exécution exorbitant par rapport à la tâche qu’il effectue ici.

De ce fait, dynamic_cast n’est pas une bonne solution, il est plus judicieux de conserver une information pour différencier les types. Comme un variant contient une liste d’éléments, l’indice du type utilisé suffit amplement.

class Variant
{
  // ...
private:
  std::unique_ptr<detail::VariantBase> impl_;
  std::size_t type_index_;
}

Maintenant, il faut convertir un type en indice, c’est à ce moment que la méta-programmation arrive à la rescousse.

#include <type_traits>

namespace detail
{
  template<class T, class... Ts>
  struct count_items_to_right_of;

  template<class T, class U, class... Us>
  struct count_items_to_right_of<T, U, Us...>
  : count_items_to_right_of<T, Us...>
  {};

  template<class T, class... Us>
  struct count_items_to_right_of<T, T, Us...>
  : std::integral_constant<std::size_t, sizeof...(Us)>
  {};
}

template<class T, class... Ts>
using mp_index_of = std::integral_constant<
  std::size_t,
  sizeof...(Ts) - detail::count_items_to_right_of<T, Ts...>::value - 1
>;

mp_index_of est un alias sur std::integral_constant. L’implémentation déroule récursivement les éléments de Ts jusqu’à trouver T et retourne le nombre d’éléments qu’il reste dans la liste. Soustraire ce résultat à sizeof...(Ts) - 1 permet d’avoir la position de T.

On met à jour l’implémentation pour initialiser le nouveau membre.

template<class... Ts>
Variant<Ts...>::Variant(Variant const& other)
: impl_(other.impl_->clone())
, type_index_(other.type_index_) // ici
{}

template<class... Ts>
template<class T>
Variant<Ts...>::Variant(T&& x)
: impl_(detail::make_variant_impl(std::forward<T>(x)))
, type_index_(mp_index_of<std::decay_t<T>, Ts...>::value) // là
{}

template<class... Ts>
Variant<Ts...>& Variant<Ts...>::operator=(Variant const& other)
{
  impl_ = other.impl_ ? other.impl_->clone() : nullptr;
  type_index_ = other.type_index_; // ici aussi
  return *this;
}

template<class... Ts>
template<class T>
Variant<Ts...>& Variant<Ts...>::operator=(T&& x)
{
  impl_ = detail::make_variant_impl(std::forward<T>(x));
  type_index_ = mp_index_of<std::decay_t<T>, Ts...>::value; // et là
  return *this;
}

Puis on supprime dynamic_cast de la fonction visit, le remplaçant par une comparaison d’index.

template<class... Ts>
template<class F>
auto Variant<Ts...>::visit(F&& f)
{
  assert(impl_);
  auto visit_impl = [&](auto rec, auto* t, auto*... ts){
    using T = std::decay_t<decltype(*t)>;
    using Impl = detail::VariantImpl<std::decay_t<T>>;
    if constexpr (sizeof...(ts)) {
      // plus de dynamic_cast, mais une comparaison d'entier + static_cast
      return type_index_ == mp_index_of<T, Ts...>::value
        ? f(static_cast<Impl*>(impl_.get())->value_)
        : rec(rec, ts...);
    }
    else {
      (void)rec;
      return f(static_cast<Impl*>(impl_.get())->value_);
    }
  };
  return visit_impl(visit_impl, static_cast<Ts*>(nullptr)...);
}

Peu de changement finalement, mais maintenant variant peut fonctionner sans support de RTTI !

On notera aussi que puisque nous possédons l’indice lié au type, on peut aussi remplacer les fonctions virtuelles par un appel à visit pour supprimer la vtable et enlever l’indirection pour accéder aux fonctions virtuelles dans celle-ci.

L’allocation dynamique n’est pas gratuite

Il est vrai que l’allocation dynamique a un coût non négligeable sur les performances. Personne n’a idée de faire new int alors qu’en regardant de plus près, c’est exactement ce que fait notre implémentation. Vient ensuite les déréférencements de pointeur qui font sauter des optimisations. Effet amplifié lorsque les fonctions sont virtual. Décidément, l’allocation dynamique pour un variant n’est pas une bonne idée.

Le mieux serait de stocker nos types de la même manière qu’une union: un seul bloc mémoire de la taille du type le plus grand. À ma connaissance il existe 2 possibilités:

Pour choisir le procédé le plus efficace, nous implémentons les 2 dans une classe qui ne possède que les fonctions d’accès et les constructeurs.

template<class... Ts>
struct AlignedStorage
{
  template<class T>
  T& get()
  {
    return *reinterpret_cast<T*>(&data);
  }

private:
  std::aligned_union_t<0, Ts...> data;
};

Sans les constructeurs, la version avec std::aligned_union est vraiment simple. Mais l’utilisation de reinterpret_cast empêche de mettre la fonction get() en constexpr (gcc l’accepte néanmoins).

À contrario, la version avec une union récursive est extrêmement verbeuse (toujours sans constructeur d’initialisation de valeur):

namespace detail
{
  template<class T, class... Ts>
  union RecursiveUnion
  {
    char dummy;
    T value;
    RecursiveUnion<Ts...> others;

    RecursiveUnion() : dummy() {}
    ~RecursiveUnion(){}
  };

  template<class T>
  union RecursiveUnion<T>
  {
    char dummy;
    T value;

    RecursiveUnion() : dummy() {}
    ~RecursiveUnion(){}
  };

  template<class T, class... Ts>
  T& get(RecursiveUnion<T, Ts...>& u)
  {
    return u.value;
  }

  template<class T, class U>
  T& get(U& u)
  {
    return get<T>(u.others);
  }
}

template<class... Ts>
struct UnionStorage
{
  template<class T>
  T& get()
  {
    return detail::get<T>(data);
  }

private:

  detail::RecursiveUnion<Ts...> data;
};

Si on regarde l’assembleur, il s’avère que les 2 versions sont exactement les mêmes.

Pour initialiser l’objet avec une valeur, la version avec std::aligned_union doit utiliser un placement new qui empêche de rendre le constructeur constexpr. Ce qui par la même occasion s’applique aussi au variant. Sans compter le problème du reinterpret_cast dans la fonction get(). Du coup, bien que cette version soit plus simple et que je ne mette pas constexpr, l’union récursive est préférable.

// std::in_place_index_t
template<std::size_t i>
struct in_place_index_t
{
  explicit in_place_index_t() = default;
};

(RecursiveUnion devient VariadicUnion)

    template<class U>
    VariadicUnion(in_place_index_t<0>, U&& x)
    : value(std::forward<U>(x))
    {}

    template<std::size_t I, class U>
    VariadicUnion(in_place_index_t<I>, U&& x)
    : others(in_place_index_t<I-1>{}, std::forward<U>(x))
    {}

Puis on adapte les fonctions de Variant. Le code final est plutôt gros alors je ne mets que le lien .

Pour éviter une condition particulière dans le code, l’union possède un membre supplémentaire: Uninit, utilisé par init, copy et destroy pour représenter un variant sans valeur.

Il y a aussi une condition dans operator= pour choisir entre la fonction copy si les 2 éléments sont du même type ou les fonctions destroy+init dans le cas contraire. Cette condition peut être supprimée si:

  • Tous les éléments ont un destructeur trivial: il n’y a pas besoin de faire destroy+init.
  • La fonction visit peut prendre plusieurs variants en paramètre pour faire un switch allant de 0 à (sizeof...(Ts) + 1) * (sizeof...(Ts) + 1).

Mot de la fin

Bien que le variant actuel soit incomplet, il est utilisable et proche des implémentations actuelles. Mais il y a plusieurs petits détails qui ne sont pas approfondis ici:

  • l’optimisation sur la taille de type_index,
  • les différents moyens de remplacer une vtable (ici je n’utilise que le if/else récursif),
  • le coût d’utilisation d’un objet en fonction de sa nature (par exemple: le compilateur dévirtualise-t-il les fonctions virtuelles venant d’un membre de variant ?)
  • les variants récursives
  • et bien d’autres

Les prochains articles seront davantage axés sur la méta-programmation et indirectement reliés avec certains aspects du variant présentés ici.

Les sources sont disponibles sur github .

Faites parler votre compilateur

Article suivant: Au cœur d'un variant
Article précédent: Ma vision de l'accessibilité appliquée pour ce blog

En C++, notre meilleur ami est le compilateur. Encore faut-il bien le configurer pour qu’il nous crache un maximum d’avertissements en pleine poire. Hélas, il s’avère que les options dépendent grandement du compilateur et de la version.

Du côté de Clang, il y a un -Weverything qui active absolument tous les warnings – dont certains que je qualifie de douteux –, alors que pour Gcc, -Wall et -Wextra n’activent pas tout. Mais pour les 2, il n’y a pas d’option qui regroupe celle de débogage ou d’analyse dynamique. Du coup, pour chaque nouvelle version, il y a un nombre plus ou moins important de flag à ajouter.

De ce constat, et parce que je possède plusieurs formats de fichier à mettre à jour, je me suis fait un générateur qu’on peut trouver sur mon github: cpp-compiler-options. Les fichiers générés sont dans le dossier output et couvrent clang-3.1 à 6.0 et gcc-4.7 à 7.1.

Petite astuce pour gcc et clang. On peut mettre des options dans un fichier et le donner au compilateur en préfixant le nom du fichier par @. Il y a un mémo dans le README.

Ma vision de l'accessibilité appliquée pour ce blog

Article suivant: Faites parler votre compilateur
Article précédent: De Blogspot à Hugo, il a changé de peau

Pour changer de la programmation logicielle, je vais parler de l’accessibilité d’un site et des petits détails exaspérants que je rencontre sur la toile. Je pense qu’une bonne partie parle à chacun, généralement on fait avec, simplement parce qu’il n’y a pas d’autre choix, mais c’est toujours frustrant de tomber dessus.

Pour le blog, j’ai passé pas mal de temps sur un template déjà existant en touchant finalement un peu à toutes les parties CSS et HTML. Je vais ici parler un peu de couleur, de police, de lien, de la manière de disposer des cadres et quelques autres bricoles insignifiantes et par définition essentielles.

Plus de clics, moins de contenu

Depuis plusieurs années, une pratique courante consiste à multiplier les clics pour accéder à du contenu. Cela prend plusieurs formes, généralement à travers un menu sectionné sur une multitude de pages, des listes déroulantes ou du texte qui apparait au clic. De l’intuitif comme ils disent.

Le défaut de vouloir tout cacher est de rendre difficile d’accès l’information:

  • Il n’est pas possible de faire une recherche de texte.
  • Si on ne sait pas dans quel menu aller, on finit par cliquer partout.
  • L’information est diffuse sur plusieurs pages, c’est beaucoup de manipulations, pénible et long. Encore plus s’il faut naviguer sur plusieurs pages avec une connexion lente.

Un des exemples typiques que je déteste est matérialisé par la FAQ de dvp: chaque catégorie doit être déroulée pour qu’apparaissent les questions. Résultat, depuis la mise en place de ce nouveau système, je ne la consulte plus puisque trouver une question approximative est une gageure. Il me faut trouver la catégorie, l’éventuelle sous-catégorie, dérouler chacune et enfin pouvoir faire une recherche de texte sur le mot qui m’intéresse dans la question (le moteur de recherche donne souvent beaucoup trop de résultats).

À contrario, les FAQs sur le site du gouvernement permettent un affichage de toutes les questions et/ou réponses ce qui permet une recherche rapide et une lecture continue.

Je trouve que le site doisjeutiliser.fr donne un bon résumé de la situation. Et je suis d’accord avec tous les autres contre-exemples.

De plus, comme le langage graphique est différent entre les logiciels et entre sites on ne sait pas toujours le comportement associé au clic ou au lien par manque d’indicateur, on va au-devant de grosses surprises. Une icône qui représente une sous-catégorie déroulée pour tel site, mais enroulée pour un autre site. Un clic pour dérouler alors que cela ouvre une nouvelle page, voire pire, une nouvelle fenêtre ou, inversement, ouvrir dans un nouvel onglet un lien fait pour afficher un zone cachée (généralement, on tombe sur une page vide). Dans les 2 scénarios c’est une mauvaise surprise.

Et certains ont la bonne idée de redéfinir (ou de simuler) les actions d’une liste déroulante en supprimant toute action clavier. Tu veux atteindre “août” en appuyant sur a ? Fais autrement ! Avec un peu de chance, il faudra aussi cliquer sur le bouton “Ok” pour valider son choix. Il ne faudrait pas user la touche entrée…

Vous l’aurez compris, je suis contre cette pratique du “clic pour apparaître” à outrance. Pour prendre en exemple le blog, sur l’ancien (blogspot) les archives sont déroulées par années et par mois. Avec une moyenne qui dépasse difficilement 2 articles/mois, en trouver un précis en moins de 2 clics mérite une médaille. Ici, les archives sont sur une page dédiée qui permet une vue d’ensemble immédiate sans étaler les titres sur plusieurs lignes.

(On y notera la police monospace sur la date qui facilite le parcours vertical ;).)

Disposition générale et débordement

De mon point de vue, un site web possède classiquement un en-tête, un menu et du contenu, le tout positionné dans un nombre restreint de possibilités. La grande différence se joue sur la gestion du vide. Càd, la place que va prendre un élément sur la page.

Il existe principalement 3 cas de figures:

  • Prendre un maximum de place.
  • Mettre une limite maximum sur la largeur.
  • Avoir une largeur fixée.

Une solution n’est pas meilleure qu’une autre, le choix dépend du design, du type de contenu et du dispositif d’affichage. La plupart du temps le site prend toute la largeur jusqu’à une taille raisonnable avant de permuter en taille fixe définie en pourcentage ou en pixel.

On peut très bien avoir un contenu qui prend toute la page quelle que soit la résolution, mais il faut être conscient qu’une ligne trop grande est difficile à lire. Plus une ligne est longue ou un paragraphe dense, moins il y a de points de repère visuels pour “suivre” le texte. Cela augmente le risque de glisser à la ligne suivante au milieu d’une phrase, de sauter une ligne ou relire la même au lieu de passer à la suivante. L’extrême inverse étant le format colonne utilisé par les journaux ou magazines, mais difficile à reproduire sur un navigateur.

Concernant la disposition du menu, à priori, n’importe laquelle fait l’affaire. À gauche, à droite ou en haut, quelle importance ? Mais c’est oublier un point essentiel: que faire quand un élément est trop grand ? La solution immédiate est simple: aller à la ligne. C’est même le comportement par défaut des navigateurs sur les blocs de texte. Ouf, nous sommes sauvés, tout va bien dans le meilleur des mondes.

Sauf qu’il existe aussi les images, les tableaux, les lecteurs vidéo et l’ensemble des éléments dont un “saut de ligne” n’est pas envisageable. Et là, c’est le drame. Heureusement, il existe plein d’horribles solutions.

Les exemples qui suivent sont faits avec une ligne de code qui ne fait pas de saut de ligne automatique.

  • S’adapter au plus grand.
    Débordement par la droite.
    Débordement par la gauche. Le scroll disparaît. Au moins pour Firefox.
  • Compacter les éléments ou mettre dans un scroll.
    Un scroll alors qu'il y a de l'espace à droite.
  • Cacher le surplus.
    Il n'y a plus moyen de lire le contenu qui déborde.
  • Superposer les éléments.
    La partie droite cache une partie du contenu.

Presque toutes ces images ont un point commun: un menu à droite. Ce n’est pas anodin, il est difficile d’avoir un menu à droite quand la partie gauche veut prendre sa place.

En déplaçant le menu à gauche, les problèmes de superposition contenu/menu disparaîssent d’eux-mêmes. On peut alors laisser le bloc prendre une place optimale.

Mais il faut le faire intelligemment pour ne pas tomber sur le premier cas qui ajoute un scroll horizontal pour l’ensemble du contenu. Vous vous imaginez devoir scroller pour lire une ligne ? Puis bon, les paragraphes qui deviennent des lignes de 3 kilomètres, non merci !

Il serait également bête de faire comme le second cas qui ajoute un scroll sur tous les éléments dépassant une certaine taille alors qu’un énorme vide persiste sur la droite.

J’ai assisté sur la doc de Hugo au passage d’un gabarit full page à celui d’une taille fixe et, malheureusement, chaque code se voyait estampillé d’un scroll horizontal. Obligé de déplacer la souris dessus pour qu’il s’agrandisse automatiquement jusqu’au bord droit de ma fenêtre. Ce qui rendait par la même occasion le scroll superflu. La seule question qui m’est venue est “Pourquoi dois-je utiliser une souris ? Pourquoi ce n’est pas l’apparence par défaut ?”.

Bon ok, deux questions.

Pour le blog, mon choix s’est porté sur un menu à gauche, un contenu sur plus au moins 60% de large pour le centrer, mais avec les contenus trop grands qui débordent un maximum avant le recourt au scroll. C’est la disposition la plus pratique à mes yeux. Le résultat peut même être visible sur l’avant-dernier article dont un code possède une ligne plus grande qu’à l’habitude.

Liens, zone de clic et surbrillance

Comment parler du web sans un petit mot sur les liens ? Le point central de la navigation internet et pourtant si difficile à cliquer pour moi.

J’ai l’impression de faire partie des gens dont la souris est invariablement attirée par la rangée de pixels non cliquable en plein milieu d’un lien. C’est systématique lorsque celui-ci est sur plusieurs lignes. Tout indique que je suis dessus: la couleur de fond du cadre qui change, celle du texte, la petite décoration qui apparaît. Tout, sauf le pointeur de souris. Mais il y a tellement de changement autour que ce détail ne se voit même plus.

Clic... Clic ? Clic clic clic ! Comment ? Je ne suis pas dessus ?

Arrive mon second fléau, déplacer suffisamment la souris pour être sur le lien. Mais pas trop, parce que ce foutu cadre qui change de couleur n’en fait pas partie non plus.

Le problème que je rencontre ici est récurant. Des changements de couleurs et de formes donnent l’impression de pouvoir effectuer une action alors que ce n’est pas vrai. Il vient alors un sentiment d’incompréhension très désagréable qui se change petit à petit en frustration lorsqu’on réalise que le pointeur de souris n’a pas la bonne forme.

Quand un cadre contenant un lien change de couleur au passage de la souris, c’est une invitation au clic. Celui-ci se doit d’être cliquable pour répondre au principe de la moindre surprise. Bonus, cela élargit la surface de clic et rend par la même occasion le lien plus accessible.

C'est quand même plus agréable lorsque le cadre est un lien.

Cette extension de la zone de clic est très présente sur le blog. Comme les espaces blancs décoratifs sur le menu qui font partie du lien. Il est possible en plein écran de plaquer le curseur à gauche et de pouvoir cliquer dessus. Ou par exemple les liens “Précédent” et “Suivant” en fin d’article qui prennent chacun une moitié de largeur de contenu. Ou encore les icônes de flux RSS qui possèdent une zone de clic réelle beaucoup plus grande que l’image. Chacun de ces éléments possède une zone de clic plus large pour simplifier la visée.

Une dernière chose à éviter impérativement sur un lien concerne l’application d’un effet de gras au survol. Si par malheur le lien se situe en fin de ligne, la mise en gras va agrandir de quelques pixels les lettres et peut déplacer des mots à la ligne suivante. Qui se retrouvent en dehors de la zone de pointage. Qui reviennent car ne sont plus concernés par le survol. Puis repartent. Et ainsi de suite rendant le lien presque incliquable pour ceux ayant échappé à la crise d’épilepsie. Bien sûr, si le lien est une boîte qui grandit ou ne se déplace pas, il n’y a aucun problème.

À présent, parlons d’une évidence: un lien doit être mis en surbrillance par rapport au reste du contenu. S’il n’y a aucun repère visuel qui le différencie, personne ne le remarquera. Dans l’imaginaire collectif, un texte bleu ou violet et surtout souligné représente un lien. Bien que l’on puisse le modifier, je déconseille d’enlever le soulignement. Un simple changement de couleur pouvant être considéré comme une mise en gras.

En plus de la pseudo-classe :hover –source des problèmes cités au début du chapitre lorsqu’elle est mal utilisée–, il est essentiel de mettre en valeur les liens possédant le focus (:focus) pour les adeptes de la navigation clavier. Certains navigateurs comme Opéra possèdent des raccourcis clavier très pratiques pour sauter de lien en lien, mais très handicapants lorsque le site n’utilise pas ce marqueur visuel.

(Je profite du paragraphe pour parler des liens d’évitement (ici et ) pour sauter les regroupements de texte comme le menu et simplifier la vie des utilisateurs de clavier et autres dispositifs).

Pour finir sur les pseudo-classes et clore le sujet, je trouve que beaucoup de sites ignorent :active, alors que c’est un excellent moyen d’informer un visiteur que son clic est bien pris en compte. C’est un indicateur qui manque souvent quand j’ouvre dans un nouvel onglet: ai-je bien cliqué ? Cette information m’est tellement essentielle que je force dans mon navigateur un cadre en pointillé rouge lorsque je valide un lien.

La police des caractères

Une chose que j’ai apprise en testant plusieurs polices d’écriture, est que la taille apparente varie énormément entre 2 fonts. Mettre une taille de police adaptée à la lecture n’est pas suffisant, il faut aussi que chaque police dans la liste de font-family ait une taille similaire au risque d’avoir des textes trop grands, ou pire, trop petits.

Je suis également tombé sur un conseil qui va à l’encontre du bon sens: mettre un font-size: 62.5% global. Avec pour seule justification que la taille par défaut est trop grande. Sans prendre en compte la taille effective d’une police et en rejetant toute configuration utilisateur. Si “62.5%” est trop petit, c’est le problème de l’utilisateur après tout.

Police16px10px (62.5%)
Sans-serif Lorem ipsum Lorem ipsum
Arial Lorem ipsum Lorem ipsum
Times New Roman Lorem ipsum Lorem ipsum
Times Lorem ipsum Lorem ipsum

Étrangement, il n’y a que sur internet où la taille est redéfinie. Sur une application de bureau personne ne s’en occupe et les polices et tailles configurées au niveau du système sont utilisées automatiquement pour les différents types d’éléments (titre, paragraphe, police monospace, etc).

Il est également possible d’utiliser les polices configurées au niveau du navigateur via leur nom générique, les principales étant serif, sans-serif et monospace.

Concernant l’empattement (serif ou sans-serif), j’en suis venu à la conclusion qu’une police sans empattement est plus lisible sur un écran. Peut-être à cause de la petitesse des lettres qui “charge” visuellement les glyphes. Ou Le rétro-éclairage, je ne sais pas.

Le pouvoir de la couleur

La couleur est un vecteur d’information aussi importante –voire plus– que la forme. Par contre, il faut utiliser la couleur d’usage sur l’objet représenté au risque de perturber le lecteur.

Perturbant, n'est-ce pas ?

La couleur et les effets permettent également de distinguer les éléments entre eux. Par exemple, une légère ombre sur un bouton lui donne un relief qui le distingue d’un cadre lambda. Mais il faut savoir varier. Si je prends comme exemple les boutons sociaux, il est difficile d’en trouver un précisément lorsqu’ils sont tous de la même couleur.

Y a-t-il ton réseau préféré ?

Alors que mettre la couleur habituellement utilisée par une marque permet de se focaliser presque sans effort dessus.

Et maintenant ?

Même si les couleurs du site sont banales, je me permets quelques remarques. Premièrement, il vaut mieux se restreindre à un nombre raisonnable de couleurs, mais suffisamment contrastées pour les dissocier. Trop de couleurs empêchent de hiérarchiser l’information, un contraste trop faible de dissocier les éléments ce qui rend illisible le texte.

Il existe différents outils pour faire des palettes de couleurs qu’on peut fabriquer avec le logiciel Agave, le très bon site Adobe Kuler, colourco.de ou code-couleur.com qui liste quelques palettes de couleurs. Ce dernier contient également la signification de certaines couleurs dans le monde occidental. Je trouve intéressant d’ajouter cet article trouvé sur la toile pour voir un peu –bien que difficile à vérifier– l’évolution et usage au fil du temps. En cherchant un peu, on tombe sur pléthore de sites et de conseils. On a l’embarras du choix.

Si je peux donner un conseil, évitez les couleurs trop pures ; celles qui contiennent une composante de couleur proche de 255 et les autres à 0 comme pour les couleurs primaires (ex: #ff0000). Ce sont des couleurs très prononcées, pas du tout naturelles. Je trouve qu’elles brillent trop et agressent les yeux.

Une dernière chose concernant les couleurs, tout le monde ne les voit pas de la même façon, essayer un filtre pour simuler le daltonisme est une expérience intéressante.

Les métadonnées sont importantes !

Les métadonnées sont toutes les informations relatives à un document qui permettent notamment de le replacer dans un contexte sans le supposer après lecture de plusieurs paragraphes (même ainsi, certains éléments sont impossibles à déduire). Elles sont tellement importantes qu’il est possible de déduire le contenu du document associé sans l’avoir jamais lu.

J’exagère un peu, mais si on prend un article de blog et qu’on a l’habitude de lire les commentaires de l’auteur sur des forums, on se fait une relativement bonne idée du contenu de ses écrits. Mais pour cela il faut au moins avoir le nom de l’auteur, le sujet principal (titre, domaine du sujet) et probablement la date. Cela paraît évident, mais des fois, un ou plusieurs éléments manquent. Grossièrement, ce sont pour moi des marqueurs qui permettent de filtrer les lectures potentiellement intéressantes et de se préparer mentalement.

Plus généralement, les métadonnées sont un moyen de mettre en relation des données avec d’autres via des URIs dans le but de faire des requêtes extrêmement sophistiquées à la manière de SQL (SPARQL). On parle aussi de web sémantique que je trouve une très bonne idée pour des ressources ayant beaucoup de relations, de dépendances, etc comme les encyclopédies ou des documents de normalisation, mais c’est un travail de longue haleine et franchement superflu dans le cadre d’un blog.

Donc voilà, les métadonnées c’est cool, mais je me contente de quelques informations en tête et pied d’article. On peut aussi les retrouver de manière plus structurée dans le flux RSS.

Parmi les informations qui gravitent autour des articles ici, on trouve:

  • Les catégories, car c’est un moyen facile de centrer le domaine d’application et filtrer les contenus.
  • La date, parce qu’une information se périme vite. Les bonnes pratiques d’hier sont les mauvaises pratiques de demain. Et si je parle d’hier, on sait au moins de quel hier je fais référence.
  • La date de dernière mise à jour s’il y a lieu. Il y a aussi une entrée dédiée dans le menu. Pour le moment je ne sais pas comment mettre en valeur les changements, peut-être une note en pied de page ?
  • Le temps de lecture. C’est un bête calcul en fonction du nombre de mots qui peut être plus précis que la taille du scroll en présence d’image. Et comme la page d’accueil contient plusieurs articles, la taille du scroll n’est pas une information fiable.
  • L’auteur, même si pour le coup il n’y aura – probablement – que moi.
  • Le lien permanent et les boutons de partage pour conquérir le monde.

Les commentaires

Sur un site statique, il n’est pas possible d’intégrer son propre système de commentaire. On peut utiliser un service web comme disqus.com qui est très bien fait, mais aussi lourd et lent à charger.

C’est un reproche qui peut se faire sur beaucoup de sites. À force d’incorporer nombre de services externes (pour certains discutables), le site est ralenti et perd en fluidité.

Comme je pense qu’un espace commentaire est toujours intéressant, je tenais vraiment à en avoir un sur le blog. Alors je me suis dit, quitte à utiliser github.io, autant y aller jusqu’au bout et me servir des issues de github comme système d’échange. Ce n’est pas aussi bien intégré dans la page qu’un système natif (il faut passer par github pour poster), mais cela fait son job. Je perds par contre les commentaires anonymes puisqu’il faut un compte pour poster.

Mais encore

À vrai dire, il y a pas mal de petites choses non citées qui m’agacent beaucoup (titre tronqué devenant trop court, animation trop lente, etc), mais ce sont des conseils qu’on peut retrouver assez facilement sur la toile.

J’ai davantage axé mes propos sur des détails que je trouve peu discutés. Tellement peu évoqués que j’ai l’impression d’être le seul concerné. Pour dire. Mais si cela permet de faire réfléchir, tant mieux.

Après, c’est bien possible que n’étant pas spécialement attiré par les technos web, un site spécialisé que je ne connais pas en parle mieux que moi. Tant mieux aussi.

De Blogspot à Hugo, il a changé de peau

Article suivant: Ma vision de l'accessibilité appliquée pour ce blog
Article précédent: Minimiser les copies dans operator+

Il y a 2 ans je me suis dit :

Au prochain article, j’essaye un autre système de blog !

Et depuis 2 ans, plus rien… Je n’ai pas parcouru le web à la recherche de la solution idéale, loin de là, je n’avais juste pas d’idée d’article.

Il y a 3 mois, en regardant une classe de matrice en C++, une idée m’est venue. J’ai écrit mon article puis cherché un système de site statique.

Au départ, j’avais en tête Octopress utilisé par Luc Hermitte pour son blog. Le principe est d’écrire des fichiers en markdown, de générer le blog et mettre le tout sur github pour avoir une adresse en github.io. Le mettre sur github n’est pas une obligation, mais c’est pour moi le plus simple.

J’ai donc essayé Hugo… Ma logique est infaillible :).

Pourquoi quitter Blogspot ?

Avant de commencer, une petite explication sur pourquoi Blogspot ne m’est pas pratique.

La principale raison est que l’utilisation de Blogspot m’oblige à faire du post-traitement sur mes articles. Pour avoir la couleur dans les codes, ils sont écrits dans un éditeur puis convertits en HTML (via Kate dans mon cas). La couleur de fond est trop contrastée pour le blog ce qui oblige un post-traitement supplémentaire. L’ensemble est plutôt rapide à faire, mais modifier un code est pénible.

Puisque je suis dans le mode HTML de blogspot, je me tape aussi tout le balisage des liens, paragraphes, mots importants, etc, ce qui parasite le texte. L’absence de couleur dans la zone d’édition n’aide pas beaucoup.

Autre point, j’écris rarement un article depuis l’interface web. Déjà parce qu’il faut y accéder – il y a un peu trop de clics à faire et Blogspot reste lent à charger – ensuite parce que l’éditeur est trop pauvre. Ça paraît con, mais les possibilités de mon éditeur de code sont très sollicités, même pour l’écriture d’un article. J’envisage même de faire des plugins pour de la saisie rapide.

Ceci fait qu’utiliser un générateur de type markdown me trottait dans la tête depuis un bon moment. Il existe plein de générateurs vers HTML, Pandoc étant probablement le plus connu. C’est suffisant. On écrit un article, on convertit et on colle la sortie dans la zone d’édition du blog.

J’aurais très bien pu utiliser Blogspot + un générateur. Sauf que je trouve Blogspot lourd à charger et les possibilités de menu et de mise en page sont un peu limitées. Alors autant voir ailleurs !

Essai de Hugo

En fait, au moment où je commençais à regarder les systèmes existants, quelqu’un sur le MM de 42 mit un lien vers Hugo. Le principe reste le même – au moins pour la génération de blog – avec comme objectif principal de ne pas avoir de dépendance et de générer rapidement le site.

Un truc qui me plaît bien est de pouvoir utiliser autre chose que le markdown par défaut, par exemple, Asciidoctor ou ReStructuredText. Les implémentations ne sont pas nativement incorporées à Hugo ce qui rend leur utilisation plus lente dans la génération (il faut appeler un programme externe). Toutefois, il y a en native 2 implémentaitons de markdown et le mode Org de Emacs. Pour se faire une idée du mode org, il y a 3 traductions françaises dont 2 à venir de Pragmatic Emacs sur linuxfr.

J’ai essayé Asciidoc, mais il ne fonctionne pas tel quel, il m’a fallu un intermédiaire qui supprime l’option --safe pour la coloraion du code (celle-ci utilise Pygmentize). Par contre, je trouve le code HTML généré vraiment trop verbeux. À mon sens, entourer les paragraphes de <div class="paragraph">...</div> est plus que superflux. Je ne sais pas ce qu’il en est des autres formats ne les ayant pas essayés, pour le moment mes besoins suffisent pour le markdown. À savoir que les formats non natifs ne peuvent pas générer le sommaire, il faut faire un post-traitement sur la sortie HTML.

Une autre chose sympathique que je n’ai pas eu l’occasion d’utiliser concerne les templates Hugo au sein même des documents. Par exemple, un template image qui sort un code HTML avec caption + figure + image de la bonne taille + lien pour voir l’image d’origine. En gros, générer un truc bien casse pied à écrire :).

Et Octopress ?

Bon Hugo c’est bon pour moi, mais j’ai rapidement fait un tour sur Octopress entre deux. Pour tout dire, j’ai rapidement abandonné. Le plugin le plus intéressant pour moi est Codeblock et il ne fonctionne pas comme indiqué. J’ai regardé les sources et finalement renoncé.

Au passage, je remarque que le projet n’est plus maintenu depuis plusieurs années et qu’il se base sur Jekyll. J’essaye ce dernier et me retrouve avec des problèmes de dépendances et des packageurs – il y en a pas qu’un – qui demandent les droits root pour l’installation. Las, j’abandonne. Finalement, Hugo c’est bien.

Pour finir

J’ai beaucoup touché au template de Hugo, aussi bien la partie CSS que HTML en me basant sur un des thèmes disponibles. À la base je voulais un thème sombre, mais j’ai finalement adopté un thème clair pour le contenu. Premièrement parce que le thème choisi l’est de base, deuxièmement parce que je trouve le résultat peu satisfaisant. Toutefois, le thème sombre est disponible en le sélectionnant dans le menu du navigateur: Vue -> Style de page -> Night theme (sur firefox, cela varie peut-être sur d’autres navigateurs). À terme, je pense chavirer du côté obscur.

La plupart des morceaux modifiés concernent des éritants que je rencontre sur certains sites et logiciels. J’ai essayé de faire quelque chose d’accessible et de pratique (là je vous vends un objet rare et de grande valeur :D). Du coup, le prochain article y sera consacré. Ça va me changer du c++ et de la méta-programmation.

Et parce que les habitudes ont la vie dure, l’article qui suivra sera dédié au dispatcheur d’un std::variant. La méta-prog, on ne la quitte que les pieds devant :o).

En même temps, les articles de l’ancien blog seront déportés et mis à jour si besoin.

Minimiser les copies dans operator+

Article suivant: De Blogspot à Hugo, il a changé de peau
Article précédent: Comment se passer de std::forward

Je vais me baser sur un classique: une classe de matrice contenant un std::vector<int>. Cette classe va implémenter 2 opérateurs mathématiques: + et +=. Le premier en fonction libre, le second en fonction membre.

Pour rigoler un peu, on ajoute une petite contrainte qui est “l’efficacité”. Petit mot qui englobe un peu tout et n’importe quoi tel que la performance en mémoire et en temps.

À vrai dire, il y a énormément de choses possibles rien que sur la structure du code: instruction vectorisée, alignement mémoire, expression template, etc. Des bibliothèques comme uBLAS, Eigen, Blitz implémentent une tripotée de choses. Ici, on va uniquement s’intéresser à la manière d’implémenter operator+ pour recycler les variables temporaires dans le but d’avoir le moins d’allocations possibles dûes aux copies.

Grosso-modo, des rvalues à droite, des rvalues à gauche, des rvalues partout et pour finir, pas de rvalue.

En réalité, il y a plusieurs approches possibles que je mets ici en opposition sans qu’elles le soient réellement.

  1. une surcharge pour tous les prototypes possibles.
  2. un opérator unique pour les gouverner tous.

Plein de surcharges de operator+

Faire 4 prototypes pour distinguer les rvalues des lvalues est un choix assez naturel. Si un prototype contient une rvalue, alors il y a moyen de recycler une valeur. On pourrait même ajouter noexcept sur de tels prototypes.

Voici ce que donne l’implémentation:

Matrix operator+(Matrix const& lhs, Matrix const& rhs)
{
  Matrix ret {lhs};
  ret += rhs;
  return ret; // et non pas return `ret += rhs`, ce qui empêcherait la NRVO.
}

Matrix operator+(Matrix&& lhs, Matrix const& rhs)
{
  lhs += rhs;
  return std::move(lhs); // ne pas oublier std::move, sinon il y a aura copie en sortie
}

Matrix operator+(Matrix const& lhs, Matrix&& rhs)
{
  rhs += lhs; // commutativité: x+y = y+x
  return std::move(rhs);
}

Matrix operator+(Matrix&& lhs, Matrix&& rhs)
{
  rhs += lhs; // éventuellement `rhs += std::move(lhs)`
  return std::move(rhs);
}

Petite note sur la dernière implémentation. Utiliser rhs comme valeur de retour permet de gagner un mov (asm). ici.

Bon, c’est bien joli, mais on peut quasiment faire la même avec seulement 2 prototypes. Seulement, pour une raison que j’ignore, ni clang, ni gcc n’applique la RVO correctement. Le constructeur de déplacement est systèmatiquement utilisé.

Matrix operator+(Matrix lhs, Matrix const& rhs)
{
  lhs += rhs;
  return lhs; // pas de RVO ???
}

Matrix operator+(Matrix const& lhs, Matrix&& rhs)
{
  rhs += lhs; // commutativité: x+y = y+x
  return std::move(rhs);
}

Les prototypes ne sont pas symétriques pour éviter les ambiguïtés. Le prototype prenant un paramètre par copie sera moins prioritaire que celui avec une rvalue, mais il accepte toutes les formes de référence.

Ainsi, si dans l’expression a + b, b une rvalue, la seconde fonction sera utilisée. Dans les autres cas, la première fonction sera utilisée. On peut facilement vérifier quelle expression correspond à quelle fonction avec un std::cout << __PRETTY_FUNCTION__ << '\n' dans les implémentations et le test qui suit.

template<class Lhs, class Rhs>
void test()
{
  std::cout << "Matrix " << (std::is_const<Lhs>{} ? "const" : "     ") << " a; ";
  std::cout << "Matrix " << (std::is_const<Rhs>{} ? "const" : "     ") << " b;\n\n";

  Lhs a;
  Rhs b;

  std::cout << std::left;
  #define C(a,b) std::cout << std::setw(13) << #a << "+ " << std::setw(15) << #b; a+b
  C(a,            b);
  C(a,            std::move(b));
  C(std::move(a), b);
  C(std::move(a), std::move(b));
  #undef C
}

int main()
{
  test<      Matrix,       Matrix>(); std::cout << "\n\n";
  test<      Matrix, const Matrix>(); std::cout << "\n\n";
  test<const Matrix,       Matrix>(); std::cout << "\n\n";
  test<const Matrix, const Matrix>();
}

Résultat:

Matrix       a; Matrix       b;

a            + b              Matrix operator+(Matrix, const Matrix&)
a            + std::move(b)   Matrix operator+(const Matrix&, Matrix&&)
std::move(a) + b              Matrix operator+(Matrix, const Matrix&)
std::move(a) + std::move(b)   Matrix operator+(const Matrix&, Matrix&&)


Matrix       a; Matrix const b;

a            + b              Matrix operator+(Matrix, const Matrix&)
a            + std::move(b)   Matrix operator+(Matrix, const Matrix&)
std::move(a) + b              Matrix operator+(Matrix, const Matrix&)
std::move(a) + std::move(b)   Matrix operator+(Matrix, const Matrix&)


Matrix const a; Matrix       b;

a            + b              Matrix operator+(Matrix, const Matrix&)
a            + std::move(b)   Matrix operator+(const Matrix&, Matrix&&)
std::move(a) + b              Matrix operator+(Matrix, const Matrix&)
std::move(a) + std::move(b)   Matrix operator+(const Matrix&, Matrix&&)


Matrix const a; Matrix const b;

a            + b              Matrix operator+(Matrix, const Matrix&)
a            + std::move(b)   Matrix operator+(Matrix, const Matrix&)
std::move(a) + b              Matrix operator+(Matrix, const Matrix&)
std::move(a) + std::move(b)   Matrix operator+(Matrix, const Matrix&)

Si on y tient vraiment, on peut ajouter Matrix operator+(Matrix&&, Matrix&&). Mais comme dit précédemment le besoin est très faible.

Un prototype multi-fonction

Une autre solution pour la surcharge d’opérateur est de ne faire qu’un seul et unique prototype template qui s’active en présence d’un certain type. Ce n’est pas une approche opposée à la précédente (elle peut servir de complément), mais je vais présenter ici comment le faire avec seulement un prototype.

Pour filtrer les types compatibles, on va utiliser la bonne vieille méthode à base de std::enable_if. Ce qui donne:

template<class MatrixLhs, class MatrixRhs>
std::enable_if_t<
  std::is_same<std::decay_t<MatrixLhs>, Matrix>::value &&
  std::is_same<std::decay_t<MatrixRhs>, Matrix>::value,
  Matrix>
operator+(MatrixLhs&& lhs, MatrixRhs&& rhs);

Dans la réalité, l’addition d’une matrice fonctionne aussi sur des entiers (cf: int + Matrix, Matrix + int). Le filtre sera alors beaucoup plus compliqué puisqu’il faut qu’au moins une des opérandes soit un type Matrix et que les paramètres soient des types compatibles (en prenant en compte la présence des références et des const). La condition devient alors quelque chose comme:

is_matrix_operand<Lhs> &&
is_matrix_operand<Rhs> &&
(is_matrix<Lhs> || is_matrix<Rhs>)

Il devient alors très facile d’ajouter un nouveau type à prendre en compte, comme par exemple un conteneur de la SL, un tableau C, un autre type matriciel d’une autre bibliothèque, etc. Faire comme dans le premier chapitre avec un prototype pour chaque cas devient vite infernal.

Il est également envisageable de faire des prototypes par catégorie de variable: Sequence et Matrix, Integer et Matrix.

Revenons-en à notre operator+ et son implémentation. Celle-ci va être plus compliqué car elle doit être équivalente aux 4 implémentations du début ; sachant que la première possède une variable et les opérandes sont inversés dans la troisième et la quatrième.

Une solution possible est de mettre 2 valeurs intermédiaires qui représentent l’opérande de gauche et l’opérande de droite et dont le type s’adapte en fonction des types en entrée.

Ci-dessous un tableau récapitulatif des types et valeurs de nos 2 nouvelles variables Lhs et Rhs. Les const sont supprimés car seule la référence importe.

Prototype NewLhs NewRhs
M & , M & M = lhs M & = rhs
M &&, M & M && = lhs M & = rhs
M & , M && M && = rhs M & = lhs
M &&, M && M && = rhs M && = lhs

Et l’implémentation:

template<class T>
struct rvalue_wrapper
{
  T& value;

  operator T&& () const noexcept
  { return static_cast<T&&>(value); }
};

template<class T>
T&& unwrap(rvalue_wrapper<T> x) noexcept
{
  return x;
}

template<class T>
T&& unwrap(T&& x) noexcept
{
  return std::forward<T>(x);
}

template<class Lhs, class Rhs>
Matrix operator +(Lhs&& lhs, Rhs&& rhs)
{
  using NewLhs = std::conditional_t<
    std::is_lvalue_reference<Lhs>::value &&
    std::is_lvalue_reference<Rhs>::value,
    Matrix,
    rvalue_wrapper<Matrix>>;

  using NewRhs = std::conditional_t<
    !std::is_lvalue_reference<Lhs>::value &&
    !std::is_lvalue_reference<Rhs>::value,
    Matrix&,
    Matrix&&>;

  constexpr bool swap_arg = !std::is_lvalue_reference<Rhs>::value;

  NewLhs new_lhs{const_cast<Matrix&>(swap_arg ? rhs : lhs)};
  NewRhs new_rhs{static_cast<NewRhs>(const_cast<Matrix&>(swap_arg ? lhs : rhs))};

  unwrap(new_lhs) += static_cast<NewRhs>(new_rhs);

  return new_lhs;
}

Le code mérite quelques explications. Pour commencer, parlons de rvalue_reference qui est un palliatif pour une optimisation au niveau de return. Au niveau du retour, si NewLhs est une rvalue, il faut utiliser std::move, sauf que l’utiliser sur une variable locale à la fonction bloque le RVO. Hélas, même avec un if (std::is_rvalue_reference<NewLhs>{}) return std::move(lhs); avant return new_lhs l’optimisation n’est pas faite. Cela fonctionne néanmoins avec if constexpr de c++17. Le but de rvalue_reference est finalement de rendre automatique un retour par rvalue grâce à l’opérateur de cast interne.

Concernant ce curieux enchaînement de cast, celui-ci s’explique par la difficulté de contrôler le type retourné par une ternaire. Une ternaire sur deux variables de même type va retourner une référence (une variable est toujours une lvalue). La référence sera considérée constante si une des deux valeurs est une référence constante. Du coup, on vire le const pour ensuite construire les types NewLhs et NewRhs.

Ici, le constructeur de la matrice (quand NewLhs = Matrix) va recevoir un type non const. À moins qu’un constructeur existe pour les références non const, cela ne cause pas de problème. On peut très bien ajouter un std::conditional pour forcer le const.

En première impression static_cast<NewRhs> pourrait être optionnel, mais celui-ci permet de forcer la rvalue pour construire NewRhs. Une lvalue (le retour de const_cast<Matrix&>) ne pouvant être affectée à une rvalue sans cela.

Les casts présents fonctionnent bien parce que lhs et rhs sont tous deux du même type. Dans le cas contraire, il faut faire un branchement à la compilation via de la surcharge de fonction (dispatch de type) tel que font falcon::cif ou boost::hana::if_. Plusieurs de mes articles en parlent.

N’écrivez pas operator+ vous-même, c’est trop compliqué !

Sérieusement, qui veut écrire une 20taine de lignes pour chaque opérateur ? Ne le faites pas, le code est alourdi, la lisibilité réduite. Il y a moyen d’implémenter la plupart des opérateurs en quelques lignes pour le même résultat.

De plus, l’implémentation des opérateurs peuvent varier. Par exemple, pas de commutativité. Ses variantes sont difficiles à détecter dans une grande masse de code, il devient facile de faire une erreur aussi bien à l’écriture qu’à la lecture.

Autre point, les types des opérandes peuvent être nombreux, faire tous les prototypes necessaires vous vaudra des heures de souffrances :).

Du coup, comment faire ? Une solution facile est d’utiliser une macro pour implémenter les opérateurs voulus. C’est simple et rapide, mais l’utiliser avec des types template est un peu délicat. Cela reste néanmoins la solution la plus simple.

Une autre manière passe par du CRTP pour que la classe de base implémente les opérateurs voulus sous forme de fonction amie. C’est la solution de boost/operators.hpp. Malheureusement, elle ne prend pas en compte les optimisations possibles sur les rvalues écrits dans cet article. Il faut la ré-écrire.

La dernière solution consiste à se servir des traits pour activer ou non certains prototypes comme dans le chapitre précédent. Une mise en oeuvre poussée peut être extrêmement extensible et s’adapte très facilement aux catégories de valeur (séquence, intégrale), mais c’est un poil complexe à mettre en place. Je ne connais pas de bibliothèque qui le fasse.

Au final, il n’existe actuellement pas d’outil satisfaisant pour générer les opérateurs alors qu’il est presque aussi rapide d’écrire une lib ou des macros pour le faire. Le temps perdu sera largement compensé par le nombre d’opérateurs à implémenter par la suite. Avec un peu de jugeote, il est même possible de mutualiser l’écriture des opérateurs @=. Pensez-y la prochaine fois qu’il faudra écrire des opérateurs ;).

Comment se passer de std::forward

Article suivant: Minimiser les copies dans operator+
Article précédent: if constexpr avant C++17

Actuellement en pleine écriture d’une fonction match (petit projet de 200 lignes actuellement), je me retrouve, pour respecter le perfect forwarding, avec une armée de std::forward utilisée à chaque appel de fonction. J’en compte 21 pour un total de 6 niveaux d’imbrications. Autant dire qu’oublier de le mettre est plutôt facile.

C’est d’ailleurs la raison de ce billet, puisque bien sûr, j’en ai oubliés.

Pour réduire leur nombre et alléger le code, je me suis fait un petit wrapper qui sauvegarde le type de référence. Comme l’objet créé a pleinement connaissance du type de variable qu’il contient et que traverser une chaîne de fonctions ne fait pas disparaître cette information, il n’y a plus besoin de std::forward. Le dernier maillon peut alors extraire une lvalue ou une rvalue via –par exemple– une fonction get().

Pour illustrer, voilà ce que cela donne:

template<class T>
struct forwarder
{
  // reference collapsing:
  // https://en.cppreference.com/w/cpp/language/reference#Reference_collapsing
  T & x;
  T && get() const { return static_cast<T&&>(x); }
};


#include <iostream>

template<class T> void print(T &&) { std::cout << "&&\n"; }
template<class T> void print(T &) { std::cout << "&\n"; }

// dernier niveau d'imbrication avant l'appel de la vrai fonction
// note: le coup d'une copie est nulle pour le type forwarder
template<class T> void f1(T x) { print(x.get()); }

// pas besoin de std::forward
template<class T> void f2(T x) { f1(x); }

// premier maillon, création du contexte
template<class T> void foo(T && x) { f2(forwarder<T>{x}); }

int main()
{
  int i = 1;
  int const ci = 2;

  foo(i); // &
  foo(ci); // &
  foo(3); // &&
  foo(std::move(i)); // &&
  foo(std::move(ci)); // &&
}

Avec 2 fonctions d’accès par référence en plus, ref et cref pour la version constante, on possède une petite classe qui simplifie pas mal la vie.

if constexpr avant C++17

Article suivant: Comment se passer de std::forward
Article précédent: Paramètres de fonction nommés en C++

Le but de if constexpr est d’interpréter le code uniquement si celui-ci respecte la condition. Il 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 une fonction invoke qui s’utilise de 3 manières:

  • foncteur + paramètres
  • fonction membre + objet + paramètres
  • fonction membre + pointeur d’objet + paramètres
// x et y sont des std::string
invoke(std::equal_to<>{}, x, y); // foncteur
invoke(&std::string::size, x, y); // fonction membre et référence
invoke(&std::string::size, &x, y); // fonction membre et pointeur

En C++17, tout peut se faire en une seule fonction, alors qu’avant C++17, il fallait faire plusieurs surcharges pour les différentes situations et jouer avec std::enable_if.

Implémentation avec if constexpr

template<class F, class T, class... Args>
decltype(auto) invoke(F&& f, T&& x, Args&&... args)
{
  if constexpr (std::is_member_function_pointer<std::remove_reference_t<F>>::value) {
    if constexpr (std::is_pointer<std::remove_reference_t<T>>::value) {
      return (static_cast<T&&>(x)->*f)(static_cast<Args&&>(args)...);
    }
    else {
      return (static_cast<T&&>(x).*f)(static_cast<Args&&>(args)...);
    }
  }
  else {
    return static_cast<F&&>(f)(static_cast<T&&>(x), static_cast<Args&&>(args)...);
  }
}

template<class F>
decltype(auto) invoke(F&& f)
{
  return static_cast<F&&>(f)();
}

Simulation de if constexpr en pre-C++17

L’atout principal de if constexpr ici est de n’évaluer le code qu’au besoin. Il faut donc un moyen de court-circuiter le flux de code. Le plus simple consiste à faire 2 fonctions, une avec un paramètre de type std::true_type, l’autre avec un std::false_type qui représente le résultat de la condition et 2 paramètres: If et Else.

Aussi, pour que les foncteurs If et Else soient évalués au dernier moment, ils devront prendre et utiliser un paramètre générique (auto). Sinon le compilateur va vérifier le code au moment de l’instanciation de la lambda plutôt qu’au moment de son utilisation.

struct Identity
{
  template<class T>
  decltype(auto) operator()(T&& x) const noexcept
  { return static_cast<T&&>(x); }
};

template<class If, class Else = int>
decltype(auto) if_constexpr(std::true_type cond, If f, Else = {})
{ return f(Identity{}); }

template<class If, class Else>
decltype(auto) if_constexpr(std::false_type cond, If, Else f)
{ return f(Identity{}); }


template<class F, class T, class... Args>
decltype(auto) invoke(F&& f, T&& x, Args&&... args)
{
  return if_constexpr(std::is_member_function_pointer<std::remove_reference_t<F>>{}, [&](auto) {
    return if_constexpr(std::is_pointer<std::remove_reference_t<T>>{}, [&](auto _) {
      return (_(static_cast<T&&>(x))->*f)(static_cast<Args&&>(args)...);
    }, /* else */ [&](auto _) {
      return (_(static_cast<T&&>(x)).*f)(static_cast<Args&&>(args)...);
    });
  }, /* else */ [&](auto _) {
    return _(static_cast<F&&>(f))(static_cast<T&&>(x), static_cast<Args&&>(args)...);
  });
}

Limitation

Cette version ne supporte pas if else, demande d’utiliser _ “là où il faut” et est syntaxiquement plus lourde. Mais la véritable limitation réside dans l’appel même d’une fonction qui ne propage pas l’usage de break, continue et return.

Paramètres de fonction nommés en C++

Article suivant: if constexpr avant C++17
Article précédent: Implémentation d'un magasin de type

Cet article est la démonstration de l’article précédent. La problématique présentée est la suivante: “Comment, dans une fonction avec plusieurs paramètres optionnels, initialiser un paramètre précis sans indiquer les valeurs optionnelles qui précèdent ?”

La fonction de référence sera la suivante:

void draw_rect(
  unsigned w, unsigned h
, char border_top = '-', char border_bottom = '-'
, char border_left = '<', char border_right = '>'
, char fill = '#'
) {
  std::cout << std::setfill(border_top) << std::setw(w+2) << "" << "\n";
  while (h--) {
    std::cout << border_left << std::setfill(fill) << std::setw(w) << "" << border_right << "\n";
  }
  std::cout << std::setfill(border_bottom) << std::setw(w+2) << "" << "\n";
}

Comment faire un appel proche de draw_rect(4,3, fill='@') ?

Création d’un paramètre nommé

La première étape consiste à créer un type par paramètre optionnel. Comme je n’ai pas envie de me compliquer la vie, la syntaxe fill='@' qui demande plus de code à cause d’une surcharge de operator= sera remplacée par un simple appel de constructeur fill{'@'}.

La définition des types devient alors véritablement simpliste:

struct border_top { char value; };
struct border_bottom { char value; };
struct border_left { char value; };

struct border_right { char value; };
struct fill { char value; };

Adapter draw_rect

Au lieu d’adapter draw_rect, je vais passer par une surcharge, ceci n’impactera pas le résultat.

La nouvelle fonction doit pouvoir prendre les nouveaux types, mais pas forcément tous et de préférence dans un ordre indéfini.

On pourrait faire toutes les surcharges possibles, il n’y a “que” plus d’une centaine de possibilités après tout… Solution rejetée, évidemment ;).

Une template variadique fera l’affaire.

template<class... Ts>
void draw_rect(unsigned w, unsigned h, Ts... params);

Il reste maintenant à associer chaque type de params avec le paramètre de notre premier prototype de draw_rect.

Distribution des paramètres

C’est là qu’intervient le magasin de type de l’article précédant (toujours pas trouvé de meilleur nom).

Le principe est simple, toutes les valeurs de params sont regroupées sous une même enseigne appelé ici “pack”. On vérifie si le pack est convertible en un type voulu et dans le cas contraire, on utilise une valeur par défaut.

Notre pack ressemble à ça:

struct Pack : Ts... {
  Pack(Ts... args) : Ts(args)... {}
} pack{params...};

Et la distribution des paramètres se fait ainsi:

draw_rect(w, h
, getval<border_top>(pack, '-')
, getval<border_bottom>(pack, '-')
, getval<border_left>(pack, '<')
, getval<border_right>(pack, '>')
, getval<fill>(pack, '#')
);

Pour les plus attentifs (il m’a fallu 2 jours pour le réaliser…), rien n’empêche d’envoyer des paramètres inutiles. On peut l’empêcher grâce à un static_assert avec une condition qui ressemble à

sizeof...(Ts) == std::is_convertible_t<border_top>()
               + std::is_convertible<border_bottom>()
               + /*etc*/

Dans l’histoire, bien que largement surmontable, getval est la fonction la plus compliquée. Si Pack est convertible en T alors Get::get() est utilisé, sinon Default::get().

template<class T, class Pack>
char getval(Pack& pack, char default_) {
  struct Get     { static char get(T item, char        ) { return item.value; } };
  struct Default { static char get(Pack&, char default_) { return default_;   } };
  return std::conditional<std::is_convertible<Pack, T>::value, Get, Default>::type
  ::get(pack, default_);
}

Aller plus loin

Les choses se compliquent quand on veut récupérer un type partiel. Un basic_fill<T> par exemple. Actuellement, rien n’existe dans le standard et il faudra jouer avec les paramètres template template et un appel conditionnel de fonction selon la validité d’une expression.

Pour compléter la solution, il faudrait aussi prendre en compte les références et ajouter des contraintes sur le paramètre.

Pour continuer dans la voie des paramètres nommés, il existe Boost.Parameters et des variantes plus modernes telles que parameter2.

Implémentation d'un magasin de type

Article suivant: Paramètres de fonction nommés en C++
Article précédent: Appel conditionnel de fonction selon la validité d'une expression

Ce que j’appelle ici un magasin de type n’est autre qu’un std::tuple où les types ne sont présents qu’une seule fois. Une espèce de set version tuple en somme.

Je me suis servi de ce type de structure à 2 reprises.

Une fois pour manipuler de façon similaire des types hétérogènes sans la lourdeur de std::tuple. Il faut dire aussi que j’étais en C++11 et que dans cette norme std::get<Type>() n’existe pas.

L’autre fois dans une fonction variadique qui distribue les valeurs vers différentes fonctions. Le but étant de ne pas se soucier de l’ordre des paramètres, certains étant optionnels.

std::tuple fait plutôt bien le boulot, mais possède un énorme inconvénient pour ce cas de figure: aucune erreur de compilation si un type est présent 2 fois (et c’est normal pour un tuple).

Planter la compilation quand un type est en doublon

Le C++ dispose déjà d’un mécanisme interne qui vérifie et hurle au scandale si un type doublon existe. J’ai nommé l’héritage.

Seulement, un héritage direct n’est pas possible avec les types scalaires, il faut un intermédiaire.

template<class T> struct item { T x; };

template<class... Ts> struct typeset : item<Ts>... {};

Avec cette implémentation, des petits malins pourraient faire de la pseudo-duplication de type en y ajoutant des qualificeurs, typeset<int, int const> par exemple.

On peut être tolérant ou devenir un tyran sans pitié en empêchant cela.

template<class... Ts>
struct tyrannical_typeset_impl : typeset<std::remove_cv_t<Ts>...> {
  using type = typeset<Ts...>;
};

template<class... Ts>
using tyrannical_typeset = typename tyrannical_typeset_impl<Ts...>::type;

Le typeset tyrannique est construit en 2 étapes, car un alias direct sur un typeset épuré ne permet pas de garder les qualificatifs.

Piocher dans le magasin

Piquer un élément du typeset est une affaire de cast. Un simple static_cast.

typeset<int, char> my_typeset;

static_cast<item<int>&>(my_typeset).x;

En mettant des opérateurs de cast dans la classe item, plus besoin de préciser cette dernière avec le static_cast.

template<class T> struct item {
  explicit operator T& () noexcept { return x; }
  explicit operator T const& () const noexcept { return x; }
private:
  T x_;
};
typeset<int, char> my_typeset;
static_cast<int&>(my_typeset);

Petit bémol toutefois, cela ne permet pas d’enlever l’ambiguïté pour un type qui diffère uniquement par son qualificatif.

typeset<int, int volatile> my_typeset;

// ‘typeset<int, volatile int>’ to ‘volatile int&’ is ambiguous
static_cast<int volatile&>(my_typeset);

Ce qu’il manque

  • Les constructeurs, évidemment.
  • Une fonction get<Type>() pour un parallèle avec la STL.
  • Une fonction pour boucler sur chaque item (for_each ?).
  • Et sûrement d’autres.

J’ai mis tout ça dans un repo au nom provisoire falcon.store.

Appel conditionnel de fonction selon la validité d'une expression

Article suivant: Implémentation d'un magasin de type
Article précédent: Réduire l'empreinte mémoire d'une agglomération de types

L’approche suivante consiste à vérifier qu’une fonction (membre ou statique) est appelable dans le but de l’utiliser, ou, à défaut, fournir implémentation générique. De manière plus générale, la méthode présentée ici s’applique à toutes expressions.

Appeler T::sort si possible, sinon std::sort(begin(T), end(T))

L’exemple va se faire sur la classe std::list qui n’est pas triable avec std::sort, mais possède une fonction membre sort(). Ainsi que sur std::vector qui, inversement, n’a pas de fonction membre sort(), mais fonctionne avec std::sort.

La méthode est simple et consiste à créer 2 fonctions: une pour vérifier si une expression est valide (ici x.sort()) et une autre en cas d’échec.

Seulement, qui dit 2 fonctions dit 2 prototypes. Leur prototype doit être légèrement différent mais compatible avec les mêmes valeurs d’entrée pour appeler la seconde si la première échoue (principe du SFINAE).

Pour vérifier l’expression, seuls 2 mots clef existent: sizeof et decltype. Cette procédure est donc possible avant C++11, même si sizeof requière un peu d’enrobage.

#include <algorithm>

//avec decltype
template<class Container>
auto dispatch_sort(Container& c, int)
-> decltype(void(c.sort())) // force decltype au type void
{ c.sort(); }

template<class Container>
void dispatch_sort(Container& c, unsigned)
{
  using std::begin;
  using std::end;
  std::sort(begin(c), end(c));
}

template<class Cont>
void sort(Cont& c)
{ dispatch_sort(c, 1); }

La fonction sort appel dispatch_sort avec un int (la valeur n’importe pas, seul le type compte). Comme la seule différence des 2 fonctions dispatch_sort est le premier paramètre, le prototype avec un int correspond parfaitement.

Si une fonction membre sort existe, alors l’expression dans decltype est valide et la fonction appelé. Dans le cas contraire, le compilateur cherche une fonction avec des paramètres pouvant être compatibles. Le int pouvant être converti en unsigned, le compilateur se rabat sur le second prototype qui fait appel à std::sort.

Le point clef étant de mettre toutes les informations dans le prototype. J’aurais par exemple pu mettre decltype dans un paramètre initialisé avec une valeur par défaut (f(int, decltype(xxx)* = 0);, mais il faudra probablement ajouter std::remove_reference car un pointeur sur une référence n’est pas permis).

Programme de test

#include <iostream>
#include <vector>
#include <list>

int main()
{
  std::vector<int> v({2,6,4});
  std::list<int>   l({2,6,4});

  sort(v);
  sort(l);

  for (auto i : v) std::cout << i << ' ';
  std::cout << '\n';
  for (auto i : l) std::cout << i << ' ';
}

Résultats:

2 4 6
2 4 6

implémentation avec sizeof (pre-C++11)

J’ai indiqué qu’il été possible d’utiliser sizeof à la place de decltype. Voici comment:

template<std::size_t, class T = void>
struct dispatch_result_type
{ typedef T type; };

template<class T>
T declval();

template<class Container>
typename dispatch_result_type<
  sizeof(void(declval<Container&>().sort()),1)
>::type
dispatch_sort(Container& c, int)
{ c.sort(); }

Le ,1 de sizeof(xxx,1) peut dérouter mais est requis si l’expression xxx retourne void. Comme void n’est pas vraiment un type, il ne fonctionne pas avec sizeof et il faut donc lui fournir autre chose. Il faut bien comprendre qu’ici xxx,1 est une seule expression et non pas 2 paramètres.

Bien que très peu probable, si j’ai mis void(yyy), c’est pour prévenir la surcharge de l’opérator ‘,’ sur le type de retour retourné par yyy (car cet opérateur peut lui-même retourner un void).

sizeof ne donne pas l’information sur le type de retour mais une valeur, il est couplé à dispatch_result_type qui prend en second paramètre template le type de retour (void par défaut). Quant à declval, c’est le même principe que celui de la SL.

Revenir en haut