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 🙂
Utiliser les save actions d’Eclipse
Certains disent du mal d’Eclipse. On dira ce qu’on veut, cet IDE propose énormément de fonctionnalités et vos collègues auront toujours de nouvelles astuces à vous faire découvrir. Une astuce que j’aime partager est l’utilisation des save actions pour automatiser des actions « pénibles » par un simple appui sur CTRL+S. Pour les activer, il suffit d’aller dans Window / Preferences… et de taper « save actions » dans le champ de recherche. Vous constaterez que chaque éditeur est configuré séparément. Pour l’exemple, je m’intéresse ici à l’éditeur Java car c’est l’éditeur que j’utilise principalement dans Eclipse. Généralement, quand je démarre un nouveau projet, je travaille comme ça :
S’il s’agit d’un projet existant, alors je choisis de ne formater que les lignes éditées. En effet, on risque de provoquer énormément de changements et donc rendre impossible la comparaison des versions du fichier dans un système de contrôle de sources. Cela ne devrait pas être le cas dans un projet bien organisé car tout le monde devrait travailler le même formateur de code et formater son code avant chaque commit dans le gestionnaire de versions des sources, mais la réalité est souvent différente.
Pour vous montrer l’intérêt, voici un bout de code taper « à la main », sans utiliser l’auto-complétion (car l’auto-complétion permet de rajouter automatiquement les imports) :
Si je l’enregistre en activant les save actions comme montré dans la première capture d’écran, le code ressemble à ça :
C’est pas mieux ? La Javadoc a été remise en forme, les espaces et tabulations ont été correctement ajoutées ou enlevées (y compris en fin de ligne, comme le montre la ligne en bleu), les accolades se mettent aux bons endroits, les imports manquant se rajoutent et résolvent les erreurs de compilations.
Vous gagnerez beaucoup de temps en utilisant les save actions. Vous pouvez être sûr de toujours avoir un code formaté correctement, conformément aux paramètres du formateur courant de l’éditeur Java. Dans la première capture d’écran, vous pouvez voir que la dernière checkbox est « Additional actions ». Vous aurez accès à encore plus d’actions !
CTRL+S, c’est la vie, et pas seulement pour ne pas perdre ce qu’on vient de faire !





