Réduire l'empreinte mémoire d'une agglomération de types

Article suivant: Appel conditionnel de fonction selon la validité d'une expression
Article précédent: 256 couleurs et plus dans la console

Un petit article pour parler d’optimisation mémoire (si on peut appeler ça comme ça) avec comme exemple la structure de donnée utilisée par std::unique_ptr.

Implémentation naïve de std::unique_ptr

Pour rappel, std::unique_ptr prend 2 paramètres template: T et Deleter (qui par défaut égal std::default_delete<T>).

Naïvement, l’implémentation serait:

template<T, Deleter = std::default_delete<T>>
class my_unique_ptr {
  T* m_pointer;
  Deleter m_deleter;
  // …
};

Rien d’extraordinaire.

Cependant, même si Deleter est une classe sans attribut, sa taille est de 1 octet.

À partir d’ici je considère que Deleter est toujours la valeur par défaut, ce qui donne:

  • sizeof(T*) == 8
  • sizeof(Deleter) == 1
  • sizeof(my_unique_ptr<T>) == 16
  • sizeof(std::unique_ptr<T>) == 8

Ouille, méchant padding, alors que seuls 8 octets sont vraiment utilisés.

Comment fait la STL pour “supprimer” 8 octets ?

La bibliohèque standard utilise une optimisation surnommée Empty Base Class Optimization (EBCO). Concrètement, cela se traduit par une classe interne qui contient le pointeur et hérite de Deleter. Les attributs de la classe dérivée vont se mettre après ceux de Deleter, et s’il n’en a pas, ils se positionnent au début de la classe. Grâce à cette astuce, l’adresse du premier membre de la classe (ici, le pointeur) se confond avec celle de la classe englobante et parente, éliminant ainsi l’espace occupé par Deleter.

template<T, Deleter = std::default_delete<T>>
class my_unique_ptr {
  struct internal : Deleter {
    T* pointeur;
  } m_data;
  // ...
};
  • sizeof(my_unique_ptr<T>) == 8

Mieux, non ?

Et si l’héritage n’est pas possible ?

Si le Deleter est une référence ou une classe final, l’héritage ne fonctionne pas. Il faut se rabattre sur la première forme (celle naïve). Avec des traits et un code plus ou moins volumineux, cela est “facile”. Il faut cependant noter que std::is_final n’apparaît qu’à partir de C++14 et son implémentation n’est pas possible en pure C++. Il faut à la place utiliser __is_final qui n’est pas standard.

Toutefois, la STL possède un conteneur générique qui utilise l’EBO si possible: std::tuple. Ce qui permet de s’affranchir de ces difficultés tout en optimisant l’espace mémoire à condition de mettre les types dans l’ordre croissant d’alignement pour réduire le padding entre les membres lorsqu’il y en a plus de 2.

256 couleurs et plus dans la console

Article suivant: Réduire l'empreinte mémoire d'une agglomération de types
Article précédent: Valeur, référence ou pointeur ? (2/2)

À chaque fois que je cherche des infos sur les couleurs je tombe toujours sur les trucs basiques. Mais j’ai récemment appris l’existence de 256 couleurs dans la console en tombant sur un dépôt contenant un $LS_COLORS particulièrement fourni.

En fait, il s’avère qu’en rajoutant extended dans la recherche “color shell” on puisse trouver quelque(s) ressource(s). J’aurais bien voulu y penser la semaine dernière, ça m’aurait évité de comprendre par tâtonnement…

Ce dernier lien ne contient pas tous les effets et le tableau de compatibilité est ancien. Par exemple, Konsole support dim color depuis quelques années et le mode 256 couleurs fonctionne sur rxvt.

Le fonctionnement des couleurs du terminal reposent sur les séquences ANSI qu’on peut aussi trouver sous l’appélation VT100 qui est devenu un standard de fait. Le principe est d’écrire certaines séquences de caractères que le terminal va interpréter comme des commandes VT100 pour – entre autres – mettre en couleur. Une commande inconnue ne fait rien.

Utilisation

echo -e "\e[${FormatColor}m"

Le caractère \e correspond à la touche Esc (échap). On peut aussi l’écrire en hexadécimal \x1b ou en octal \033.

Pour bash et echo, il faut utiliser l’option -e pour interpréter les séquences backslashés ou utiliser la forme $'\x1b'.

${FormatColor} correspond à un nombre expliqué dans les chapitres suivant et représentant une couleur de texte, de fond ou un effet. Plusieurs formats peuvent être mis en les séparant par des ; (point virgule).

Style/effet de texte

Effet Code Code annulation
normal 0
gras 1 21
sombre ** 2 22
italique * 3 23
souligné 4 24
clignotant 5 25
couleur texte/fond inversée 7 27
caché ** 8 28
barré * 9 29

Les effets avec une seule étoile ne sont pas supportés dans quelques rare terminaux et ceux avec 2 étoiles ont un support variable.

Le code 6 (clignotant rapide) n’étant pas supporté, il n’est pas listé ici.

À savoir que le code 0 (normal) supprime également toutes les couleurs.

8 couleurs

C’est le mode de couleur supporté par la majorité des terminaux. La couleur réellement affichée peut également être configurée au niveau du terminal lui-même ce qui explique pourquoi selon les environnements toutes les couleurs ne sont pas similaires.

Couleur Texte Fond
Noir 30 40
Rouge 31 41
Vert 32 42
Jaune 33 43
Bleu 34 44
Magenta 35 45
Cyan 36 46
Gris clair 37 47
Défaut 39 49

Les valeurs 38 et 48 représentent un mode étendu expliqué plus tard.

16 couleurs

Ce sont 8 couleurs supplémentaires accessibles pour certains terminaux. Les couleurs réelles derrière peuvent également être configurées dans le terminal.

Couleur Texte Fond
Gris foncé 90 100
Rouge clair 91 101
Vert clair 92 102
Jaune clair 93 103
Bleu clair 94 104
Magenta clair 95 105
Cyan clair 96 106
Blanc 97 107

88 et 256 couleurs

C’est un mode étendu, certains vieux terminaux limitent à 88 couleurs, mais la plupart en supportent 256. Il s’utilise avec le triplet suivant:

  • Texte: 38;5;${x}
  • Fond: 48;5;${x}

Dont ${x} est à remplacer par un nombre allant de 0 à 255 inclus.

TrueColor

Il est possible d’utiliser le classique RGB quand le terminal le permet. Beaucoup d’interfaces consoles ne le prennent pas bien en charge ou ne le détectent pas. Pour la seconde catégorie, il faut généralement activer une option.

  • Texte: 38;2;${r};${g};${b}
  • Fond: 48;2;${r};${g};${b}

Dont ${r}, ${g} et ${b} sont à remplacer par un nombre allant de 0 à 255 inclus.

Appliquer la couleur seulement en mode interactif

La couleur est bien pratique en mode interactif, mais peut devenir gênant lorsqu’on enchaîne les grep et sed. La plupart des commandes ont une option --color qui prend auto, always et never pour forcer ou non l’utilisation des couleurs. auto permet de mettre la couleur uniquement si la sortie est un terminal. En bash, le test ce fait comme ceci:

if [[ -t 1 ]]; then
  echo stdout est un terminal
else
  echo stdout n\'est pas un terminal
fi

Il est ensuite facile de définir des variables qui contiennent les séquences ANSI correspondantes.

error=$'\e[31m'
reset=$'\e[0m'
if [[ ! -t 1 ]]; then
  error=
  reset=
fi

echo "${error}oups${reset}"

Exécuter le script va afficher “oups” en couleur, alors que faire quelque chose comme ./lescript.sh | cat n’affichera plus de couleur puisque grâce au |, la sortie n’est plus un terminal, mais l’entrée de la commande cat.

Notes de fin

Comme TrueColor et les couleurs étendues ne fonctionnent pas partout, le terminal peut interpréter la valeur des couleurs comme des commandes VT100 et détériorer le rendu final. Dans une telle situation, il faut le réinitialiser avec tput reset.

Voici un bout de code qui permet de visualiser une palette de 256 couleurs.

for m in 38 48 ; do
  n=0
  for l in {0..31} ; do
    for c in {0..7} ; do
      echo -ne "\033[$m;5;$n;1m$n\e[0m\t"
      ((++n))
    done
    echo
  done
done

Valeur, référence ou pointeur ? (2/2)

Article suivant: 256 couleurs et plus dans la console
Article précédent: Valeur, référence ou pointeur ? (1/2)

Dans le précédent billet, j’opposai les paramètres par références constantes à ceux par valeurs.

Sans plus attendre entamons la seconde question.

Référence ou pointeur ?

Si je dois faire court je dirai: pointeur jamais ; référence quand possible. Sans autre forme de procès :D

Ah, on me dit dans l’oreillette qu’il faut argumenter… C’est parti.

Les références possèdent un contrat beaucoup plus fort que les pointeurs: elles ne peuvent être nulles et référencent toujours la même variable.

À contrario, les pointeurs peuvent changer la variable référencée ou ne pointer sur aucune valeur (nullptr).

Pour le dire autrement, une référence est l’équivalent d’un pointeur constant non-nul (avec une syntaxe d’utilisation plus simple: pas besoin de déréférencer). De leurs restrictions, celles-ci ne peuvent pas toujours correspondre au besoin ; les pointeurs sont alors envisageables.

De plus, les pointeurs sont beaucoup utilisés dans les constructions dynamiques (allocation dynamique) quand les classes sont à sémantique d’entités. Principalement car ces dernières ne sont pas copiables et que l’allocation dynamique permet de s’affranchir de la portée (le scope) en se détachant de la pile.

Le pointeur parle trop

L’usage de pointeur (pointeur nu) est cependant à prendre avec des pincettes, voici 3 questions que soulève l’usage d’un pointeur:

  • Dois-je contrôler la durée de vie du pointeur (le détruire) ? [oui, non]
  • Est-ce un élément ou une séquence d’élément ? [séquence, simple valeur, ça dépend]
  • Le pointeur peut-il être nul ? [oui, non]

Après un petit calcul combinatoire (2*3*2), il y a 12 réponses possibles. Le pire est de répondre: “ça dépend”. Si on l’enlève, il reste quand même 8 possibilités.

La sémantique du pointeur est, au final, très faible. Lui en ajouter devient alors capital.

Plus de sémantique pour un pointeur

Hélas, il n’y a pas de réponse universelle, tout dépend des cas d’usages. De plus certaines combinaisons son conceptuellement douteuses. Pour exemple, un pointeur non-nul mais qu’on détruira. Le non-nul amène aux références mais idéologiquement une référence n’est pas faite pour être détruite.

On peut néanmoins sortir quelques règles:

  • Si la valeur référencée existe toujours et que l’appelé ne gère pas la durée de vie une référence fait l’affaire. Attention toute fois avec les références constantes sur temporaire expliquées dans le billet précédent. Selon la situation, std::reference_wrapper qui permet de changer la référence utilisée ou quelque chose comme gsl::non_null peuvent aussi correspondre.
  • Si l’appelé contrôle la durée de vie: pointeurs intelligents (std::unique_ptr en priorité, std::shared_ptr, …).
  • Si le pointeur peut être nul et que l’appelé ne gère pas la durée de vie alors un pointeur est “justifié”. En interne du moins, pour l’extérieur un non_owner_ptr ou un observer_ptr sera plus parlant. Si le pointeur peut être invalidé pendant l’exécution alors std::weak_ptr ou autres du même genre est à envisager.
  • Tout ce qui est tableau est indiqué dans les signatures des objets wrapper (unique_ptr<T[]>) ou/et grâce à un attribut de taille. De plus, s’il faut soit des tableaux, soit une valeur alors toujours préférer le type commun: tableau (les valeurs deviennent des tableaux de taille 1). Les tableaux dynamiques sont, quant à eux, plus faciles à utiliser avec std::vector.

Au final, l’usage de pointeur nu est très peu utilisé, voire pas du tout. De plus, leur mauvais usage avec l’allocation dynamique amène des fuites mémoires principalement dues aux libérations manuelles. Dans un langage comme le C++, un code non exception safe va faire des fuites mémoires. De manière générale, la libération s’applique sur toute forme de ressource: lock, fichier, etc.

Pour éviter cela, les ressources doivent être attachées à la pile et le déterminisme de destruction permettra de les libérer convenablement. On parle aussi de RAII. Pour rappel, tout ce qui est sur la pile est détruit à la sortie du scope. La sémantique de déplacement permettra de changer de portée.

Le wiki de Guillaume Belz en parle très bien: pourquoi le RAII est fondamental en C++ ?

Valeur, référence ou pointeur ? (1/2)

Article suivant: Valeur, référence ou pointeur ? (2/2)
Article précédent: Parcourir les arguments d'une fonction variadique

Quand utiliser une variable par valeur, référence ou pointeur ?

Telle fut la question qui m’a été posée :p.

Comme je ne suis pas entièrement satisfait de la réponse que j’ai donné, je fais un article. Pour tout dire, la réponse n’est pas aussi triviale que l’on pourrait le croire depuis l’arrivée du C++11 et la sémantique de déplacement.

Tout d’abord, décomposons cette question en 2 parties:

  • Valeur ou référence constante ?
  • Référence ou pointeur ?

Je réponds ici à la première, la seconde fera l’objet d’un autre article.

Valeur ou référence constante ?

Le choix se justifie en majorité par un besoin d’optimisation. Une prise par valeur induit forcément une copie, cette dernière pouvant être extrêmement coûteuse. Par exemple, la copie d’un std::vector ne se fait pas en un claquement de doigt, il y a tout un attirail derrière: allouer un espace mémoire et copier tous les éléments du vecteur précédent qui peuvent eux-mêmes faire des opérations complexes.

À contrario, la référence constante est un alias vers une variable. Il n’y a jamais de copie.

J’insiste bien sur référence constante car, pour être au plus proche de l’effet d’une copie, l’objet d’origine ne doit pas bouger. De plus, une instance constante ne peut appeler que des fonction membre constantes, ce qui assure une invariance (cf: const-correctness).

Donc, référence constante pour les valeurs qui ne sont pas modifiés dans la fonction. Une règle dit: “tout ce qui est plus grand qu’un pointeur pourrait être passé par référence constante”. Je préfère dire tous les types en référence constante sauf les fondamentaux (int, float, etc). Même si mettre une référence constante sur un int n’est pas une erreur, je n’adhère pas vraiment.

Une seule exception cependant, quand le paramètre va de toute façon être copié localement dans la fonction pour être modifié. On pourrait croire que le résultat sera le même, mais c’est être naïf.

class BigInt {  };
operator+(const BigInt & a, const BigInt & b)
{
  BigInt ret(a);
  ret += b;
  return ret;
}

Supposons que BigInt fasse de l’allocation dynamique pour représenter les nombres. Avec ce code:

BigInt n1(1);
BigInt n2 = BigInt(2) + n1;

Il y a 3 allocations:

  • n1
  • BigInt(2)
  • ret

Alors que cette implémentation de operator+ n’en produit que 2.

operator+(BigInt a, const BigInt & b)
{
  a += b;
  return a;
}

Car il y a copy elision (c’est le même principe que la RVO mais pour les paramètres). Voir aussi ici et la réponse de Flob90.

Mais ça c’était avant…

Maintenant qu’il y a la sémantique de déplacement, les copies sont préférées quand une fonction recevant le paramètre va, quoi qu’il arrive, le copier dans une variable membre. Premièrement parce que l’utilisateur pourra faire un std::move de sa variable pour s’en “débarrasser” car il n’en a plus besoin. Deuxièmement parce que la fonction a besoin d’une copie et le compilateur le fera pour nous.

En comparaison avec une copie sur le std::vector, le move-constructor et le move-assignment sont extrêmement rapide: 3 affections de pointeur pour chaque vecteur.

Par exemple avec cette base:

#include <utility>
#include <vector>

using vector_int_t = std::vector<int>;

class A {
  vector_int_t c;

public:
  A(vector_int_t cont)
  : c(std::move(cont))
  {}
};

Le code suivant fait 2 allocations (comme avec les références constantes)

vector_int_t c{1, 2};
A a(c);

Alors que celui-ci qu’une seule

vector_int_t c{1, 2};
A a(std::move(c));
// c.size() == 0;

Et ce dernier aussi

A(vector_int_t{1, 2});
// ou A({1 ,2});

Mais si le type ne possède pas de constructeur de déplacement, celui de copie sera utilisé et alors une référence constante est probablement mieux.

Quand la copie se fait sous condition

Il existe des paramètres pouvant être copiés, mais pas toujours. Dans ce cas, bien que la référence constante reste une bonne solution, une version prenant aussi une temporaire (rvalue ici) est probablement mieux. Mais si les types ne sont pas abstraits (comprendre full template) alors il faudra faire 2 versions: une avec rvalue et une avec constref. Ce qui se traduit, quand le code est un peu long, par l’ajout d’une fonction de prédicat ou une version template privée appelé par les 2 autres.

Les && sur les types full templates ont 2 états possibles: rvalue ou lvalue (catégorie de valeurs).

struct Foo {
  void foo(std::string const& s) { privfoo(s); }
  void foo(std::string&& s) { privfoo(std::move(s)); }

private:
  template<class String>
  // ici && représente soit rvalue, soit une référence (constante)
  void privfoo(String&& s) {
    // …
    if (xyz) {
      // soit un move-assignment soit un copy-assignment
      str = std::forward<String>(s);
    }
  }

  std::string str;
};

Ou

struct Bar {
  void bar(std::string const& s) { if (check(s)) str = s; }
  void bar(std::string&& s) { if (check(s)) str = std::move(s); }

private:
  bool check(std::string const& s) {
    // …
    return xyz;
  }

  std::string str;
};

Prise d’objet non-copiable sous condition

Par exemple, donner la propriété d’un std::unique_ptr à une classe selon certains prérequis décidés par une fonction. Contrainte supplémentaire, l’objet n’est pas copiable.

  • Une référence constante n’est pas envisageable, l’objet ne pouvant pas être déplacé car constant.
  • Une prise par valeur non plus (grâce à std::move), car la ressource serait systématiquement transmise à la fonction même si cette dernière ne la garde pas. L’appelant est dans l’incapacité de le savoir et perd la ressource.
  • Une référence non-constante est possible, mais il ne sera alors pas possible d’envoyer un temporaire. Il faudra obligatoirement passer par une variable intermédiaire ce qui est désagréable quand on ne va rien en faire.
  • Reste la rvalue avec laquelle une temporaire fonctionne, mais il faudra automatiquement faire un std::move quand la variable est une référence. Cela a l’avantage d’informer l’utilisateur sur l’éventuel déplacement de ressource.

Au final, bien qu’une référence fonctionne, seule une rvalue est pratique à l’usage. Seulement, aucunes de ces méthodes n’indiquent une prise partielle, seule la documentation nous le dira. Ceci pourrait par contre être une convention d’écriture: si une ressource non copiable est prise par rvalue, alors la fonction est libre de se l’approprier quand certaines conditions internes sont remplies.

Quand les opérations ne sont pas connues

Reste la dernière situation: les templates. Les règles sont les mêmes qu’avant mais si le rôle du paramètre n’est pas défini – comprendre que la fonction ne fait rien d’autre qu’envoyer le paramètre à une autre fonction ou que le qualifier importe peu, – les paramètres sont à prendre par référence universelle (T&& pour les template). À ce moment, toutes les utilisations de cette variable devraient se faire par l’intermédiaire de std::forward, même lorsqu’une fonction membre est utilisées (la faute au qualificateur de référence sur fonction membre).

Toutefois, attention de ne pas déléguer plusieurs fois la responsabilité et de ne faire std::forward (et std::move) que sur la dernière utilisation de la variable.

Cependant, certains objets de par leur concept seront pris par valeur. Comme les itérateurs pour la bonne raison que leurs états changent dans l’execution de la fonction. On peut toutefois prendre l’itérateur de fin par référence constante s’il n’est pas modifié.

Pas de référence constante pour les observers

Bien que cela sorte du cadre de la question d’origine, il ne faut pas prendre par référence constante une valeur à observer.

J’entends par observer les variables qui sont gardées en lecture dans le but de vérifier leur état à un instant t.

Les références constantes peuvent être des temporaires à leur construction (Jusque-là c’est défini par la norme: prolongement de la durée de vie d’une temporaire). Le problème vient du déplacement vers un scope parent. La valeur temporaire est détruite, mais la référence est gardée ; référence sur une valeur qui n’existe plus. Cela débouche sur un comportement indéfini et, dans le meilleurs des cas, un segfault.

Un article qui présente une situation similaire avec une lambda retournant T à travers std::function<const T&>.

#include <iostream>
#include <string>

struct Validate
{
  const std::string & s;
  void display() const { std::cout << s; }
};

Validate f()
{ return {"plop"}; }

int main()
{
  Validate x = f();
  x.display();
}

(Moi j’ai un segfault)

Pour limiter ce bug, il faut empêcher de prendre une rvalue. Soit avec les constucteurs suivants:

Validate(std::string&&)=delete;
Validate(const std::string & s):s(s){}

Soit en utilisant std::reference_wrapper (et std::cref).

Ou, peut-être mieux, faire un objet observable tout pareil que std::reference_wrapper, mais avec constructeur explicite. L’intérêt d’utiliser l’un des 2 objets cités et de focaliser l’utilisateur sur l’aspect “j’ai besoin que cette variable vive au moins aussi longtemps que moi”.

Résumé

  • Valeur pour les types fondamentaux (int, double, etc), ceux modifiés sans que l’utilisateur n’ai besoin de le savoir (ex: itérateurs ou premier paramètre de l’opérateur ‘+’) ou les foncteur sans états (prédicats, comparateurs, …).
  • Valeur + std::move quand l’objet peut être déplacé (possède un move-ctor ou/et move-assign non trivial (ex: std::string, std::vector, …).
  • Valeur pour les ressources non copiables à transférer (std::unique_ptr, …).
  • Référence constante pour les paramètres en lecture seule ou ne disposant pas de move-ctor ou/et move-assign non trivial.
  • Référence constante et rvalue quand le paramètre peut être copié et possède un move-ctor/move-assign non trivial.
  • Rvalue pour les ressources non copiables avec déplacement conditionnel (std::unique_ptr, …).
  • Référence constante pour les types inconnus (template) qui n’ont pas d’intérêt à être pris par valeur (ou au pire, stratégie variable selon le résultat de std::is_trivially_*/std::is_copy_*/std::is_move_*).
  • Rvalue pour les types inconnus (template) quand le paramètre n’a pas de rôle direct dans la fonction ou que le qualificateur n’importe pas (ne pas oublier std::forward pour le transmettre à une autre fonction (seulement s’il n’est plus utilisé ensuite)).

Partie 2

Parcourir les arguments d'une fonction variadique

Article suivant: Valeur, référence ou pointeur ? (1/2)
Article précédent: Tableau dans un std::vector

À l’approche de Noël et du déballage de cadeaux, faisons un tour sur le déballage des paramètres variadiques.

Info : Ce qui suit n’est plus d’actualité depuis C++17 et les fold expressions.

Fonction récursive

La méthode habituelle pour utiliser chaque paramètre est la récursion jusqu’à plus d’argument ou jusqu’à un nombre défini, généralement 1.

Quelque chose dans ce goût-là:

#include <iostream>

void f1()
{}

template<class T, class... Args>
void f1(const T& first, const Args&... others)
{
  std::cout << first << '\n';
  f1(others...);
}

template<class T>
void f2(const T& first)
{
  std::cout << first << '\n';
}

template<class T, class... Args>
void f2(const T& first, const Args&... others)
{
  std::cout << first << ", ";
  f2(others...);
}

int main()
{
  f1(1, 2.4, "plop");
  f2(1, 2.4, "plop");
}

Ce qui affiche:

1
2.4
plop
1, 2.4, plop

Sans récursivité

Il existe cependant une autre façon de faire qui n’utilise pas la récursivité. J’ai découvert cette méthode sur le forum de openclassroms (ce sujet, 10ème message), elle est très astucieuse.

Implémentée sous la forme d’une macro cela donne:

#define UNPACK(...)                   \
  (void)::std::initializer_list<int>{ \
    (void((__VA_ARGS__)), 0)...       \
  }
  • Le premier void permet d’ignorer l’initializer_list créé.
  • (void((__VA_ARGS__)), 0)... évalue l’ensemble de l’expression, ignore le résulat (grâce au void) et retourne un caractère dans l’initalizer_list.
  • Au final, l’expression est dépaquetée et l’initializer_list est rempli de 0.

Avec cette macro les 2 fonctions précédentes deviennent:

template<class... Args>
void f1(const Args&... args)
{
  UNPACK(std::cout << args << '\n');
}

template<class T, class... Args>
void f2(const T& first, const Args&... others)
{
  std::cout << first;
  UNPACK(std::cout << ", " << others);
  std::cout << '\n';
}

Ce qui simplifie considérablement l’écriture :).

Évidemment, cette macro se trouve depuis dans falcon: falcon.cxx.

Fold expression et C++17

Depuis C++17, le langage introduit les fold expressions qui ne font pas moins que la macro précédente. Elles font même un peu plus, mais je vous laisse consulter la doc pour en prendre connaissance.

template<class... Args>
void f1(const Args&... args)
{
  ((std::cout << args << '\n'), ...);
}

template<class T, class... Args>
void f2(const T& first, const Args&... others)
{
  std::cout << first;
  ((std::cout << ", " << others), ...);
  std::cout << '\n';
}

La structure du code est très proche du précédent, il n’y a que la macro qui est remplacée par une paire de parenthèse et , ....

Par contre, il ne faut pas oublier que pour certain type, l’opérateur , peut être surchargé et engendrer des comportements indésirables. La solution consiste – comme précédemment – à entourer l’expression de void. Comme cela devient un peu moche, on recycle la macro UNPACK:

#define UNPACK(...) (void((__VA_ARGS__)), ...)

Tableau dans un std::vector

Article suivant: Parcourir les arguments d'une fonction variadique
Article précédent: Utilisation de swap et des fonctions utilitaires en général

Prenons le type suivant: std::vector<int[4]>. Qui peut se justifier. À priori, cela ne cause aucun problème ; et c’est vrai !

Ajoutons maintenant un élément à notre vector avec push_back.

std::vector<int[2]> v;
int a[2]{};
v.push_back(a);

Patatras, rien ne va plus, il y a 2 erreurs. La première concerne la construction du tableau et la seconde sa destruction car un tableau n’a ni constructeur ni destructeur.

La manière la plus facile pour éliminer ces erreurs de compilation est de mettre un wrapper sur le tableau. Écrire ce wrapper n’est pas très compliqué et Oh joie, Oh bonheur, il existe std::array.

Mais si l’on tient vraiment à notre tableau (pour d’obscures raisons sataniques :D) il est toujours possible de modifier le comportement du vector pour qu’il comprenne les tableaux. Ceci à travers l’allocateur, le second paramètre template d’un vector ; celui jamais utilisé, toujours oublié std::vector<T, là_ici>.

La technique consiste à remplacer l’allocateur par une version spécialisée pour les tableaux. Le plus simple est d’hériter d’un std::allocator et de redéfinir les 2 méthodes problématiques: construct et destroy. On peut aussi utiliser une spécialisation de template.

Et ne surtout pas oublier rebind qui permet de recréer l’allocateur avec un type interne différent. C’est utilisé par les conteneurs :).

(Un précédent article permet de comprendre les mécanismes utilisés par un allocateur.)

L’implémentation de construct et destroy est vraiment bateau, il suffit d’appeler le constructeur ou le destructeur pour chaque élément du tableau.

Mais pour faire au minimum bien les choses et supporter l’allocation de tableau de tableau (int[3][2]) on utilise array_allocator::construct/destroy si les cellules sont des tableaux (appel récursif) ou std::allocator::construct/destroy dans le cas contraire.

À noter que la récursivité peut être éliminée en utilisant les propriétés d’alignement des tableaux (int[3][2] -> int[3*2]).

template<typename T>
class array_allocator;

//c'est juste pour les tableaux
template<typename T, std::size_t N>
class array_allocator<T[N]>
: public  std::allocator<T[N]>
{
public:
  using value_allocator = typename std::conditional<
    std::is_array<T>::value,
    array_allocator<T>,
    std::allocator<T>
  >::type;

public:
  template<typename U>
  struct rebind
  { typedef array_allocator<U> other; };

  void construct(T(*arr_ptr)[N], const T(&val)[N])
  {
    value_allocator alloc;
    for (std::size_t n = 0; n < N; ++n) {
      alloc.construct(&(*arr_ptr)[n], val[n]);
    }
  }

  void destroy(T(*arr_ptr)[N])
  {
    value_allocator alloc;
    for (std::size_t n = 0; n < N; ++n) {
      alloc.destroy(&(*arr_ptr)[n]);
    }
  }
};

Dans l’idéal, il faudrait un construct avec un nombre variable d’arguments ce qui permet d’utiliser emplace_back. Toutefois, cette version est fonctionnelle avec push_back :).

std::vector<int[3], array_allocator<int[3]> > v;
int a[3]{1,2,3};
v.push_back(a);
v.push_back({4,5,6});

(La bibliothèque falcon (dont je suis le seul développeur actuellement :p) dispose d’un allocateur de ce style: generic_allocator. Comme son nom l’indique, l’allocateur couvre un spectre un peu plus large et gère en plus des types POD.)

Utilisation de swap et des fonctions utilitaires en général

Article suivant: Tableau dans un std::vector
Article précédent: make sans Makefile, utilisation des règles implicites

Une fonction utilitaire est une fonction libre qui n’étant pas attachée à une classe particulière comme une fonction de conversion (to_string, etc) ou un accesseur externe à une classe (get, begin, end, etc).

Toutes ces fonctions citées sont disponibles en C++11. La seule fonction utilitaire qui me vient à l’esprit en C++03 est std::swap (fonction qui échange le contenu de 2 variables). Pour info, l’en-tête de std::swap est passé de <algorithm> en C++03 à <utility> en C++11.

Comment déclarer et utiliser une fonction utilitaire

Il arrive qu’un jour ou l’autre on veuille faire une surcharge de swap pour un objet particulier. À ce moment 2 choix s’offrent: en faire une fonction libre dans le namespace où se trouve la classe ou dans le namespace std. Évidemment, le meilleur choix est le premier.

Et là, pour toutes les personnes qui ont en horreur le using namespace std ou travaillant dans les .h, un problème va se poser. Comme swap se trouve dans le namespace de la STL, il est logique de faire std::swap(...). Mais à ce moment, la fonction swap spécialisée dans le namespace de la classe n’est pas utilisée…

#include <iostream>
#if __cplusplus >= 201103L
# include <utility>
#else
# include <algorithm>
#endif

namespace my {
  struct A{};
  void swap(A&, A&)
  { std::cout << "ok\n"; }
}

int main()
{
  my::A a, b;
  std::swap(a,b);
}

N’affiche rien… :/.

Par contre ce qui suit affiche “ok”.

int main()
{
  my::A a, b;
  swap(a,b);
}

Comme une fonction swap prenant des my::A existe, elle est utilisée. Cela est possible car la fonction swap fait partie du namespace my et que les variables a et b sont des instances d’une classe du même namespace.

Alors que ceci ne compile pas.

int main()
{
  int a, b;
  swap(a,b);
}

Donc, d’un côté on a un swap générique dans la STL et de l’autre un swap spécialisé dans my. Et surtout, 2 syntaxes différentes.

Dans un contexte générique (typiquement des templates) et de maintient de code, 2 formes, ce n’est pas acceptable. Il faudrait que std::swap soit utilisé si aucun swap spécialisé n’existe.

Pour ce, on “déplace” std::swap dans le scope courant avec using et grâce l’ADL (Argument-dependent lookup), le compilateur appellera la bonne fonction.

int main()
{
  using std::swap;
  {
    my::A a, b;
    swap(a,b); //affiche ok
  }
  {
    int a, b;
    swap(a,b); //compile
  }
}

Et ceci s’applique pour toutes les fonctions libres.

Simplifier l’utilisation

La nécessité de “déplacer” les fonctions dans le scope est lourd et facile à oublier.

L’idéal serait la présence d’une unique fonction en charge d’appeler la bonne surcharge. La solution consiste en la création d’un namespace dans lequel la fonction générale est exportée et où une fonction intermédiaire l’appelle. Comme la règle d’ADL s’applique, la fonction surchargée sera appelée si existante.

#include <algorithm>

namespace fn {
  namespace adl_barrier {
    using std::swap;

    template<class T>
    void swap_impl(T& a, T& b)
    { swap(a,b); }
  }

  template<class T>
    void swap(T& x, T& y)
    { ::fn::adl_barrier::swap_impl(x,y); }
  }
}

Cependant, cette solution souffre de l’effet inverse: la fonction ne peut pas être déplacée dans le même scope qu’une fonction généraliste. Il y aurait 2 prototypes identiques, le compilateur ne pourrait pas lever l’ambiguïté. Comme il est très courant de voir using namespace std;, un using fn::swap dans le même scope rentrerait en conflit avec std::swap lors de l’appel (aucun problème si dans un sous-scope).

#include <iostream>

namespace my {
  struct A{};
  void swap(A&, A&)
  { std::cout << "ok\n"; }
}


int main()
{
  {
    my::A a, b;
    fn::swap(a,b); // affiche ok
  }
  {
    int a, b;
    fn::swap(a,b); // compile
  }

  {
    using fn::swap;
    using std::swap;
    int a, b;
    swap(a,b); // ambiguïté
  }

  {
    using std::swap;
    {
      using fn::swap;
      my::A a, b;
      fn::swap(a,b); // affiche ok
    }
  }
}

make sans Makefile, utilisation des règles implicites

Article suivant: Utilisation de swap et des fonctions utilitaires en général
Article précédent: Optimisation de script bash en limitant l'ouverture de processus

La commande GNU make possède énormément de commandes implicites regroupées dans un Makefile “par défaut” avec plein de règles. Celui-ci est visible en tapant make -pf /dev/null dans un terminal.

Rien qu’avec ça, on peut compiler des fichiers C, C++, archive, latex, etc. Il y a de quoi faire en fait.

Par exemple, je crée un fichier C nommé test.c:

int main(int ac, char** av)
{
  return ac;
}

Que je compile avec make test pour créer l’exécutable test. Mais si on veut avoir des fichiers objets, il faut le demander explicitement: make test.o test.

Maintenant, on remplace test.c par test.cpp et on inclut <iostream>.

La compilation se fait encore avec make test ou make test.o test pour avoir les fichiers objets.

Ah ! En fait non, le second ne fonctionne pas. test.o est compilé avec cc, et comme <iostream> est linké avec une lib, l’implémentation du constructeur std::ios_base::Init n’est pas trouvée. L’idéal serait d’utiliser g++ ou un autre compilateur C++ pour avoir les chemins de bibliothèque convenablement configurés.

Si ce n’est qu’une question de compilateur, changeons-le !

Tout d’abord, la règle de compilation d’un .o vers un exécutable:

make -pf/dev/null | grep '%: %.o' -A3
%: %.o
#  commands to execute (built-in):
        $(LINK.o) $^ $(LOADLIBES) $(LDLIBS) -o $@

Cela utilise pour compiler la variable LINK.o. Soit, qu’est sa valeur ?

make -pf/dev/null | grep 'LINK.o ='
LINK.o = $(CC) $(LDFLAGS) $(TARGET_ARCH)

Qui fait référence à CC. On s’approche, vérifions:

make -pf/dev/null | grep 'CC ='
CC = cc

Bingo \o/. Il suffit donc de changer la valeur de CC.

On recommence avec make CC=g++ test.o test et ça fonctionne :).

Cette petite excursion permet mine de rien de découvrir pas mal de variables. Au passage, il est plus intéressant de les utiliser que d’en recréer des nouvelles. Idem dans les règles de dépendances. S’il faut les personnaliser, autant garder les mêmes commandes à exécuter.

Par exemple, pour compiler un .cpp en .o:

%.o: %.cpp
#  commands to execute (built-in):
        $(COMPILE.cpp) $(OUTPUT_OPTION) $&lt;

Généralement les sources sont également dépendantes de fichier d’en-tête. Si on fait une règle qui prend en compte cela, autant garder la même commande: (petite parenthèse pour dire que gcc possède l’option -MM pour lister les dépendances entre les fichiers.)

bidule.o: bidule.cpp bidule.h machin.h
        $(COMPILE.cpp) $(OUTPUT_OPTION) $&lt;

Ainsi, quand CXXFLAGS sera modifié, la règle le prendra en compte car

COMPILE.cpp = COMPILE.cc

Et

COMPILE.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c

À noter que la variable CPPFLAGS fait référence à la commande cpp (ou option -E de gcc) et concerne le C-preprocessor.

Bien sûr, la technique du Makefile seul a des limites. Par exemple, s’il y a plusieurs fichiers C, je peux créer tous les fichiers objets avec make *.o, mais l’exécutable ne sera dépendant que d’un fichier. Au final les règles par défaut ne permettront pas de créer l’exécutable, il faudra faire gcc *.o ou un Makefile avec au minimum a.out: *.o.

Voilà, ce fut un petit post pour découvrir l’existence des règles implicites et des variables prédéfinies :).

Manual de make

Optimisation de script bash en limitant l'ouverture de processus

Article suivant: make sans Makefile, utilisation des règles implicites
Article précédent: Référence constante sur référence

Une des choses qui prend du temps dans l’exécution d’un script shell est le nombre de programmes appelés et par conséquent, le nombre de processus créés.

Le meilleur moyen d’accélérer un script est de passer par les builtins et limiter les boucles ouverture/fermeture de programme. En fait, dans certains cas, on pourrait avoir une commande qui lit sur l’entrée standard et retourne un résultat ; un peu comme bc. C’est là que les coprocessus viennent à la rescousse :).

Un coprocessus est un sous-shell exécuté de façon asynchrone et fournissant les flux d’entrée/sortie. Ceux-ci sont accessibles via $COPROC[1] et $COPROC[2].

Du coup, avec bc, la méthode est d’écrire dans un flux et lire dans l’autre. Comme la lecture est bloquante, le script va attendre que bc retourne le résultat.

Et voilà ! L’ouverture d’un programme est remplacé par une lecture/écriture.

Pour exemple, un petit programme qui lit un fichier contenant des opérations mathématiques et les affichent suivies du résultat.

#!/bin/bash
[ -z "$1" ] && echo "$0: file" >&2 && exit 1

#coprocessus de bc avec la sortie d'erreur redirigée vers la sortie standard
coproc bc 2>&1

while read l; do
  echo "$l" >&${COPROC[1]} # écrire dans le processus
  read result <&${COPROC[0]} # réception du résultat
  echo "$l = $result"
done < "$1"

Sans coproc, la boucle est plus simple, mais un processus sera ouvert pour chaque ligne du fichier, ce qui est particulièrement coûteux.

while read l; do
  echo -n "$l = "
  echo "$l" | bc
done < "$1"

À tester avec par exemple un fichier de calcul comme celui-ci:

2+3
2+3*9
23*9
23s*9
3*9

Référence constante sur référence

Article suivant: Optimisation de script bash en limitant l'ouverture de processus
Article précédent: Placement new, allocateur et conteneur

Une petite note sur les références et le qualificatif const en commençant par un exemple :).

using reference = int&;
int i = 0;
reference const r = i;
r = 3;

Contrairement à ce que laisse croire le code, il est possible de modifier la valeur de r. En fait, ajouter const sur une référence ne fait rien car cela ne s’applique pas sur le référé qui reste un int&.

Si on déroule complètement le type de r, on obtient int & const r: une référence constante sur un entier. Comme une référence est par nature immutable (le référé ne peut pas changer), le const est superflux. Le type de r est donc bien une référence sur un entier non-constant.

(On notera toutefois que la syntaxe int & const est invalide puisque const ne peut s’appliquer directement sur une référence.)

Revenir en haut