C++

Formater du texte en C++ avec {fmt}

Formater du texte en C++, c’est pas ultra fun… Il y a bien les « bons » vieux reliquats du C avec les fonctions genre std::printf() et les méthodes un peu plus modernes comme std::ostringstream. C’est pas génial comparer à string.format() de Python En regardant le code de spdlog( une bibliothèque de logging qui semble très bien mais qui malheureusement n’était pas compatible avec mes contraintes), j’ai découvert la bibliothèque {fmt}. C’est elle qui sert de back-end à spdlog pour formater les messages. Si vous voulez essayer {fmt}, voici le mode opératoire !

Builder la bibliothèque

Tout le code est sur GitHub, est placé sous licence BSD et se builde avec CMake. Le mode opératoire est donc très classique :

$ git clone https://github.com/fmtlib/fmt.git
$ cd fmt
p$ mkdir build
$ cd build
$ cmake ..
[...]
$ make all
Scanning dependencies of target fmt
[  2%] Building CXX object CMakeFiles/fmt.dir/src/format.cc.o
[  4%] Building CXX object CMakeFiles/fmt.dir/src/posix.cc.o
[  7%] Linking CXX static library libfmt.a
[  7%] Built target fmt
Scanning dependencies of target gmock
[  9%] Building CXX object test/CMakeFiles/gmock.dir/gmock-gtest-all.cc.o
[ 12%] Linking CXX static library libgmock.a
[ 12%] Built target gmock
Scanning dependencies of target test-main
[ 14%] Building CXX object test/CMakeFiles/test-main.dir/test-main.cc.o
[ 17%] Building CXX object test/CMakeFiles/test-main.dir/gtest-extra.cc.o
[ 19%] Building CXX object test/CMakeFiles/test-main.dir/util.cc.o
[ 21%] Linking CXX static library libtest-main.a
[ 21%] Built target test-main
Scanning dependencies of target time-test
[ 24%] Building CXX object test/CMakeFiles/time-test.dir/time-test.cc.o
[ 26%] Linking CXX executable ../bin/time-test
[...]
[100%] Linking CXX executable ../bin/util-test
[100%] Built target util-test

$ make test
Running tests...
Test project /home/pgradot/Documents/GitHub/fmt/build
      Start  1: assert-test
 1/11 Test  #1: assert-test ......................   Passed    0.02 sec
      Start  2: gtest-extra-test
 2/11 Test  #2: gtest-extra-test .................   Passed    0.03 sec
      Start  3: format-test
[...]
      Start 10: posix-mock-test
10/11 Test #10: posix-mock-test ..................   Passed    0.20 sec
      Start 11: posix-test
11/11 Test #11: posix-test .......................   Passed    4.85 sec

100% tests passed, 0 tests failed out of 11

Total Test time (real) =   5.84 sec


$ sudo make install
[  7%] Built target fmt
[...]
[100%] Built target format-impl-test
Install the project...
-- Install configuration: "Release"
-- Installing: /usr/local/lib/cmake/fmt/fmt-config.cmake
-- Installing: /usr/local/lib/cmake/fmt/fmt-config-version.cmake
-- Installing: /usr/local/lib/cmake/fmt/fmt-targets.cmake
-- Installing: /usr/local/lib/cmake/fmt/fmt-targets-release.cmake
-- Installing: /usr/local/lib/libfmt.a
-- Installing: /usr/local/include/fmt/core.h
-- Installing: /usr/local/include/fmt/format.h
-- Installing: /usr/local/include/fmt/format-inl.h
-- Installing: /usr/local/include/fmt/locale.h
-- Installing: /usr/local/include/fmt/ostream.h
-- Installing: /usr/local/include/fmt/printf.h
-- Installing: /usr/local/include/fmt/time.h
-- Installing: /usr/local/include/fmt/posix.h

J’ai volontairement raccourci la sortie de plusieurs commandes (vous avez sans doute vu les […]). Vous n’êtes pas obligé de faire make test mais c’est sympa de vérifier que notre bibliothèque s’est correctement compilée. On voit que l’installation a copié dans des dossiers classiques la bibliothèque statique libfmt.a ainsi que les nombreux headers dont nous aurons besoin pour l’utiliser dans notre code. Vous pourriez ne pas l’installer et récupérer les fichiers pour les mettre dans votre projet ou encore utiliser directement le CMake de {fmt} comme un sous-CMake de votre projet.

Petits essais

J’ai repris quelques lignes données dans le README du GitHub et j’ai écrit une petite fonction log() puisque c’est un peu pour ça que je me suis intéressé à {fmt} :

#include 
#include 
#include 

namespace pgt
{
template 
void log(const char* format, Args&& ... args)
{
	try
	{
		auto now = std::time(nullptr);
		auto timestamp = std::string(std::ctime(&now));
		std::replace(timestamp.begin(), timestamp.end(), '\n', '\0');

		auto message = fmt::format(format, args...);
		fmt::print("[{}] {}\n", timestamp, message);
	}
	catch(const fmt::format_error& e)
	{
		fmt::print("Invalid log message: {}\n", e.what());
	}
}
}

int main()
{
	// Exemples de GitHub
	fmt::print("Hello, {}!\n", "world");
	fmt::printf("Hello, %s!\n", "world");
	std::string s = fmt::format("{0}{1}{0}\n", "abra", "cad");
	fmt::print(s);

	// Log
	int speed = 1200;
	pgt::log("Speed = {} rpm - Temperature = {}°C", speed, 42);
	pgt::log("Setpoint = {}");
}

On compile et on lance le programme :

$ g++ -Wall -Wextra -std=c++11 main.cpp -lfmt && ./a.out
Hello, world!
Hello, world!
abracadabra
[Fri May 11 11:59:13 2018] Speed = 1200 rpm - Temperature = 42°C
Invalid log message: argument index out of range

Notes :

  1. C’est pas une super bonne idée de faire une fonction qui s’appelle log dans le namespace global, elle risque d’entrer en conflit avec std::log, qui dans mon cas était disponible (le test a été fait avec un gcc 5.4). Ainsi, un appel à log(42) compilait sans erreur alors que pgt::log(42) génère bien un message d’erreur.
  2. Il y a moyen de faire encore mieux pour formater le timestamp des logs mais mon vieux compilateur semblait perdu…
  3. Si on intervertit main.cpp et -lfmt dans la ligne de commande, on se mange une palanquée d’erreurs de link.
  4. Oui, un formatage peut rater et lancer des exceptions !

Personnellement, je trouve ça bien sympa !

Pour aller plus loin, vous pouvez regarder la documentation de l’API ou approfondir sur la syntaxe du formatage,

C’est tout pour aujourd’hui 🙂

Publicités

C++17 et fold expression

Il y a quelques jours, j’ai découvert les fold expressions, une nouveauté de C++17. J’ai trouvé ça cool. Je me suis surtout demandé à quoi ça pourrait me servir. Et puis j’ai pensé à un code que j’avais écrit il y a quelques semaines et et je me suis dit que ça aurait sans doute été plus simple avec ça. Je ne vais pas modifier le vrai code puisqu’il marche et est déjà utilisé dans un logiciel professionnel mais j’ai fait un code d’essai pour voir. Et effectivement, c’est bien mieux ! Je vais vous montrer ça.

Le but est de monitorer des variables pendant l’exécution du programme. J’ai pour cela une classe Monitor à laquelle on peut ajouter des variables à surveiller. Voici à quoi ressemble le code :

#include <iostream>

class Monitor {
public:
    void watch(short& s [[maybe_unused]]) {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }

    void watch(int& i [[maybe_unused]]) {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }

    void watch(long& l [[maybe_unused]]) {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }
};

int main(void) {
    Monitor m;

    short s = 0;
    m.watch(s);

    int i = 0;
    m.watch(i);

    long l = 42;
    m.watch(l);
}

J’en profite au passage pour vous montrer l’extensions GNU __PRETTY_FUNCTION__ qui permet d’afficher joliment le nom de la fonction et une utilisation de l’attribut maybe_unused (une autre nouveauté de C++17) pour à éviter le classique (void) paramètre_qui_ne_sert_pas dans le corps des fonctions. On a ainsi un code qui compile sans warning et qui produit la sortie suivante :

void Monitor::watch(short int&)
void Monitor::watch(int&)
void Monitor::watch(long int&)

À l’utilisation, il s’avère qu’il est pratique de pouvoir ajouter plusieurs variables d’un coup, par exemple en écrivant m.watch(s, i, l);. L’idée est de créer une fonction template prenant un nombre variable de paramètres, chacun pouvant être d’un type différent des autres. C++11 a apporté cette possibilité, grâce aux variadic templates et aux parameter packsUne telle fonction ressemblerait à ça :

template<typename...Targs>
void function(Targs... args) {
}

Ça se corse quand on souhaite itérer sur les éléments du pack… La technique classique consiste à fait faire des appels récursifs à la fonction pour réduire le pack. Pour la classe Monitor, il convient donc de rajouter deux fonctions :

class Monitor {
public:
    // idem

    template<typename T, typename...Targs>
    void watchSeveral_recursive(T& t, Targs&...args) {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
        watch(t);
        watchSeveral_recursive(args...);
    }

private:
    template<typename T>
    void watchSeveral_recursive(T& t) {
        std::cout << __PRETTY_FUNCTION__ << " (final) " << std::endl;
        watch(t);
    }
};

La fonction publique est effectivement récursive. L’astuce est d’avoir un paramètre simple en plus du pack. Chaque appel imbriqué va ainsi « réduire le pack », puisque le premier élément du pack va devenir le premier argument et que le reste du pack va créer un nouveau pack en deuxième argument. Comme toute récursion, il faut un cas d’arrêt : c’est le rôle de la fonction privée. Elle est appelée quand le pack ne contient plus qu’un élément puisque le compilateur va préférer cette surcharge privée à la surcharge publique dans un tel cas. Ça donne ça à l’utilisation :

int main(void) {
    Monitor m;

    short s = 0;
    int i = 0;
    long l = 42;

    m.watchSeveral_recursive(s, i, l);
}
void Monitor::watchSeveral_recursive(T&, Targs& ...) [with T = short int; Targs = {int, long int}]
void Monitor::watch(short int&)
void Monitor::watchSeveral_recursive(T&, Targs& ...) [with T = int; Targs = {long int}]
void Monitor::watch(int&)
void Monitor::watchSeveral_recursive(T&) [with T = long int] (final) 
void Monitor::watch(long int&)

Sympa, hein ?

Voici maintenant la version avec une fold expression :

class Monitor {
public:
    // idem

    template<typename...Ts>
    void watchSeveral_withFoldExpr(Ts&...args) {
        std::cout << __PRETTY_FUNCTION__ << std::endl;
        (watch(args), ...);
        // Notez l'utilisation de parentheses !
    }
};

Elle s’utilise évidemment de la même manière que précédemment :

int main(void) {
    Monitor m;

    short s = 0;
    int i = 0;
    long l = 42;

    m.watchSeveral_withFoldExpr(s, i, l);
}

La sortie est bien sûr ce qu’on s’attend à obtenir :

void Monitor::watchSeveral_withFoldExpr(Ts& ...) [with Ts = {short int, int, long int}]
void Monitor::watch(short int&)
void Monitor::watch(int&)
void Monitor::watch(long int&)

Voilà, voilà… J’avais dit que c’était mieux ^^ C’est tellement simple que je n’ai pas besoin de vous expliquer le code. Il suffit que de savoir que c’est une fold expression pour le comprendre. C’est une fonctionnalité toute nouvelle, peu de gens la connaissent, mais vous oui après la lecture de cet article 🙂


Laisse faire ton compilateur, il sait ce qu’il fait :)

Ce qui est bien avec les compilateurs modernes, c’est qu’ils sont très bons pour optimiser le code à notre place. Un bon code est un code efficace mais simple, que le compilateur comprendra et analysera facilement. Le développeur pourra décider d’écrire un code « plus optimisé » si et seulement si le compilateur n’a pas réussi à produire quelque chose de satisfaisant. Mais il devrait quand même d’abord essayer de clarifier son code 😉

J’en ai encore fait l’expérience ces jours-ci en me demandant si un ranged-loop for (apparu en C++11) sur un tableau avait un overhead par rapport à un parcours classique avec un index de 0 à taille-1. Je suis donc aller sur Compiler Explorer et j’ai écrit ce petit code :

void f(int v);

constexpr int SIZE = 128;
int array[SIZE];

void g() {
    for(auto& e : array) {
        f(e);
    }
}

void h() {
    for(int i = 0; i < SIZE; ++i) {
        auto& e = array[i];
        f(e);
    }
}

Je l'ai ensuite compilé avec GCC 7.2.1 pour ARM, puisque c'est la toolchain que j’utilise sur mon projet actuel. Avec l’optimisation à O2 ou O3, les codes assembleurs générés sont les mêmes. En revanche, une des versions est un poil meilleure en O1…. et ce n’est pas la version classique mais la version mode du ranged-loop for. Sans doute parce qu’il n’y a aucune ambiguïté sur le code : il s’agit bien de parcourir le tableau élément par élément. La différence est minime toutefois 😉

Voici ce que ça donne (les couleurs montrent la correspondance code source / code assembleur) :

 

Conclusion : laissez faire votre compilateur, il sait ce qu’il fait…. et sans doute bien mieux que vous !


std::optional du C++17

C++17 a apporté son lot de nouvelles fonctionnalités et l’une est l’ajout de std::optional<class T>. Comme son nom l’indique très bien, c’est une classe template qui contient une valeur optionnelle. Pratique quand vous voulez écrire une fonction qui renvoie une valeur et en même temps signaler qu’il est possible que cette valeur soit invalide et ne doive pas être utilisée. Pour plus de détails sur cette fonctionnalité, vous pouvez bien sûr lire le très bon article de cppreference.

Pour l’exemple, j’ai fait écrit une fonction qui tente de charger une image. Comme le chargement peut échouer, elle ne renvoie pas directement une instance de la classe Image mais un std::optional<Image> pour wrapper une image qui peut ne pas être présente. Voici le code :

Rien de bien sorcier. Une classe Image avec ici juste le chemin vers l’image. La fonction load_image() regarde si le chemin qu’on lui fournit correspond à une image connue et renvoie alors une image ; sinon, elle renvoie std::nullopt, une constante magique pour créer un objet optionnel absent. La fonction try_to_load() montre comment on peut ensuite se servir d’un std::optional. Un tel objet possède un opérateur de conversion en booléen pour tester s’il contient une valeur ou pas. Alternativement, vous pouvez utilisez sa méthode has_value(), qui a une sémantique plus explicite. Une fois le test fait, la méthode value() permet de récupérer l’objet wrappé, ici une instance d’Image qui a donc une méthode getPath().

Petit détail important, l’objet wrappé est contenu dans le std::optional. Ce dernier ne contient pas un pointeur vers la valeur et aucune allocation dynamique n’est effectuée ici.

Personnellement, je trouve ça sympa 🙂


C vs C++ : initialisation des variables statiques

On lit souvent C/C++. Hé hé… S’il est indéniable que le ++ de C++ n’est pas là pour rien, il faut aussi savoir qu’il y a des différences en forme de pièges dans la partie « commune ». Ces différences sont particulièrement trompeuses quand on écrit du code C++ qu’on souhaite être compatible C. On va s’intéresser aujourd’hui à l’initialisation des variables statiques.

Voici un code C++ a priori anodin :

#include <stdlib.h>

int getValue()
{
    return rand();
}

int f(int c)
{
    static const int initial = getValue();

    if (initial != c)
        return c;
    else
        return initial;
}

int main()
{
    return f(42);
}

La ligne intéressante est la ligne 10 : on initialise une variable statique avec la valeur de retour d’une fonction.

Plaçons ce code dans un fichier main.cpp, plaçons ce fichier dans un projet Eclipse, et laçons un build :

[pre]16:52:21 **** Incremental Build of configuration Debug for project Experiences ****
Info: Internal Builder is used for build
g++ -std=c++0x -O0 -g3 -pedantic -Wall -Wextra
-c -o « src\\main.o » « ..\\src\\main.cpp »
g++ -o Experiences.exe « src\\main.o »

16:52:21 Build Finished (took 498ms)[/pre]

Renommons simplement le fichier pour forcer sa compilation en C. En effet, GCC se base sur l’extension du fichier pour savoir comment le compiler. Voici le résultat du build dans ce cas :

[pre]16:53:12 **** Incremental Build of configuration Debug for project Experiences ****
Info: Internal Builder is used for build
gcc -std=c99 -O0 -g3 -pedantic -Wall -Wextra
-c -o « src\\main.o » « ..\\src\\main.c »
..\src\main.c: In function ‘f’:
..\src\main.c:10:32: error: initializer element is not constant
static const int initial = getValue();
^

16:53:12 Build Finished (took 139ms)
[/pre]

L’erreur est claire. On pourrait soupçonner le qualificatif const mais cela ne change rien si on l’enlève. Le résultat est le même si la variable est statique au fichier au lieu d’être statique à la fonction.

Sans aller creuser dans les spécifications des langages, on se doute que C résoud la valeur d’une variable statique à la compilation alors que C++ autorise qu’elle soit résolue à l’exécution. Pour cela, le compilateur va sans doute générer un peu de code pour savoir si la variable a déjà été initialisée et l’initialiser le cas échéant (inutile de préciser que cela a un coût). On peut le vérifier en regardant le code assembleur de la fonction f() :

[pre] f(int):
0040144d: push %ebp
0040144e: mov %esp,%ebp
00401450: sub $0x18,%esp
10 static const int initial = getValue();
00401453: mov $0x40d020,%eax
00401458: movzbl (%eax),%eax
0040145b: test %al,%al
0040145d: jne 0x40148a
0040145f: movl $0x40d020,(%esp)
00401466: call 0x4014c4
0040146b: test %eax,%eax
0040146d: setne %al
00401470: test %al,%al
00401472: je 0x40148a
00401474: call 0x401440
00401479: mov %eax,0x40d028
0040147e: movl $0x40d020,(%esp)
00401485: call 0x4014bc
12 if (initial != c)
0040148a: mov 0x40d028,%eax
0040148f: cmp 0x8(%ebp),%eax
00401492: je 0x401499
13 return c;
00401494: mov 0x8(%ebp),%eax
00401497: jmp 0x40149e
15 return initial;
00401499: mov 0x40d028,%eax
16 }
[/pre]

Changeons l’initialisation de notre variable par quelque chose de constant, par exemple :

static const int initial = 666;

Voici le nouveau code assembleur, beaucoup plus simple, de la même fonction f() :
[pre] f(int):
0040144d: push %ebp
0040144e: mov %esp,%ebp
12 if (initial != c)
00401450: cmpl $0x29a,0x8(%ebp)
00401457: je 0x40145e
13 return c;
00401459: mov 0x8(%ebp),%eax
0040145c: jmp 0x401463
15 return initial;
0040145e: mov $0x29a,%eax
16 }
[/pre]

Pour avoir un code similaire en C, il faudrait implémenter une protection similaire, comme ceci :

#include <stdbool.h>
int f(int c)
{
    static int initial;
    static bool guard = false;

    if (guard == false)
    {
        guard = true;
        initial = rand();
    }

    if (initial != c)
        return c;
    else
        return initial;
}

On voit quelques inconvénients : perte de lisibilité, perte du qualificatif const, aucune garantie de thread-safety. A noter qu’en C++, le caractère thread-safe d’un tel code semble dépendre de la version du langage, comme débattu dans cette discussion stackoverflow « Is local static variable initialization thread-safe in C++11?« .

C’est tout pour aujourd’hui, vous êtes prévenus 🙂

Édition du 05/04/2017 : il s’avère en fait que la nécessité d’un initializer constant en C ne se limite pas aux variables déclarés avec le mot-clé static. Cette contrainte s’applique à toutes les variables ayant une static storage duration, comme expliqué dans cette discussion stackoverflow « Error “initializer element is not constant” when trying to initialize variable with const« . Ainsi, cette contrainte s’applique aussi aux variables globales.