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 :
- C’est pas une super bonne idée de faire une fonction qui s’appelle
logdans 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 quepgt::log(42)génère bien un message d’erreur. - Il y a moyen de faire encore mieux pour formater le timestamp des logs mais mon vieux compilateur semblait perdu…
- Si on intervertit
main.cppet-lfmtdans la ligne de commande, on se mange une palanquée d’erreurs de link. - 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 🙂
Le warning -Wwrite-strings de gcc
Petit article pour parler d’un warning intéressant bien que non nécessaire de gcc : -Wwrite-strings.
La documentation de gcc précise l’effet de cette option :
-Wwrite-strings
When compiling C, give string constants the type const char[length] so that copying the address of one into a non-const char * pointer produces a warning. These warnings help you find at compile time code that can try to write into a string constant, but only if you have been very careful about using const in declarations and prototypes. Otherwise, it is just a nuisance. This is why we did not make -Wall request these warnings.
Testons son effet avec un bout de code trouvé sur Stackoverflow :
#include
#include
void somefunc(char buffer[10]) {
int i;
for (i = 0; i < 10; i++)
buffer[i] = 0;
}
int main(void) {
somefunc("Literal");
return 0;
}
Si je compile ce code normalement, c’est-à-dire uniquement avec les options -Wall -Wextra, le compilateur ne génère aucun warning. En revanche, j’obtiens le message suivant en ajoutant l’option -Wwrite-strings :
D:\...\main.c|13|warning: passing argument 1 of 'somefunc' discards 'const' qualifier from pointer target type [enabled by default]|
C’est bien car on va essayer d’écrire dans un string litteral, ce qui va forcément pas bien marcher (doux euphémisme pour dire qu’il va y avoir une erreur de segmentation). Bien que le warning ne le dise pas explicitement, il attire quand même l’œil sur un problème de type : un pointeur possède le qualificatif const (le paramètre réel) et l’autre non (le paramètre formel). Comme il est bien sûr interdit d’ignorer un warning sans avoir une bonne raison, on se rendra compte du problème.
Il faut toutefois se méfier des faux positifs qui peuvent être générés. Il faut être rigoureux sur l’utilisation du mot-clé const dans les prototypes de fonctions, comme dit par la documentation. Voici un exemple de faux positif donné par kwariz :
void print_string(char *str)
{
printf("string : '%s'\n", str);
}
Si la fonction est appelée avec une string litteral en paramètre, comme exemple ainsi print_string("Hello world");, le compilateur émettra un warning. En effet, le mot-clé const n’est pas respecté mais ici, ce n’est pas grave puisqu’on n’essaye pas de modifier la chaîne. La solution n’est en revanche pas d’ignorer le warning ou d’enlever l’option : il faut rajouter le mot-clé const dans le prototype de la fonction puisque la zone pointée n’est pas modifiée. Cela supprimera le warning et ne posera pas de soucis si la fonction est appelée avec une chaine modifiable. Le code suivant ne produit pas de warning par exemple :
#include
#include
void print_string(const char *str)
{
printf("string : '%s'\n", str);
}
int main(void) {
print_string("Literal");
char msg[] = "Not litteral";
print_string(msg);
return 0;
}
Bon ajout de const et au revoir quelques erreurs de segmentation 🙂


