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 🙂


