Apprendre Rust quand on vient du C++ : afficher une chaîne
Cela fait plusieurs mois que je travaille sur un projet de conversion de C++ vers Rust à l'aide de LLM. Le projet comporte une très grande quantité de code relativement bas niveau et, malgré les milliers de lignes de Rust que j'ai vues passer, je suis encore incapable d'en écrire une seule moi-même. J'ai décidé de faire quelque chose qu'on avait l'habitude de faire dans l'ancien monde à savoir apprendre :-)
Je ne vais pas vous mentir je ne voulais pas commencer cette série d'articles par le passage obligé du Hello world mais finalement j'ai du modifier mes plans car de toute façon j'aurais du l'évoquer plus tard.
Si vous etes du genre impatient je vous fais le résumé de comment afficher une chaîne:
| Rust | C | C++ | C++23 |
|---|---|---|---|
print! | printf(...) | std::cout << ... | std::print("...") |
println! | printf("...\n") | std::cout << ... << std::endl | std::println("...") |
eprint! | fprintf(stderr, ...) | std::cerr << ... | std::print(stderr, "...") |
eprintln! | fprintf(stderr, "...\n") | std::cerr << ... << std::endl | std::println(stderr, "...") |
A partir de là vous pourriez vous dire, c'est bon c'est facile le rust mais en vrai la syntaxe devrait vous déranger surtout le ! à la fin.
Pourquoi println! n'est pas une fonction ?
Quand on découvre Rust après plusieurs années de C ou C++, l'une des premières choses qui surprennent est l'affichage dans la console :
println!("Hello World");
Le point d'exclamation intrigue immédiatement :
println!("Hello World");
^
Pourquoi y a-t-il un ! ?
La réponse est simple : println! n'est pas une fonction. C'est une macro.
Une macro, c'est quoi ?
Une macro est un mécanisme qui permet de générer du code pendant la compilation.
On peut voir une macro comme une fonction qui travaille sur le code source lui-même.
Par exemple :
println!("Age = {}", age);
est transformé par le compilateur en un code plus complexe avant d'être compilé.
L'idée est similaire aux macros du préprocesseur C :
#define MAX(a,b) ((a) > (b) ? (a) : (b))
mais les macros Rust sont beaucoup plus sûres et beaucoup plus puissantes.
Pourquoi ne pas utiliser une fonction ?
La première raison est que Rust ne possède pas de fonctions variadiques générales comme le C.
En C, on peut écrire :
printf("%d %s\n", age, name);
Le nombre d'arguments peut varier.
En Rust, une fonction classique devrait avoir une signature fixe :
fn afficher(...) {
}
Or :
println!("Bonjour");
println!("Age = {}", age);
println!("{} {}", prenom, nom);
acceptent un nombre différent d'arguments.
Une macro peut analyser ces arguments à la compilation et générer le code approprié.
Vérification du format à la compilation
L'un des avantages majeurs des macros Rust est qu'elles permettent au compilateur de vérifier la cohérence des chaînes de format.
Ceci compile :
let age = 42;
println!("Age = {}", age);
Mais ceci provoque une erreur de compilation :
let age = 42;
println!("Age = {} {}", age);
Erreur :
2 positional arguments in format string, but there is 1 argument
Le compilateur détecte immédiatement le problème.
En C :
printf("%d %d\n", age);
l'erreur n'est généralement détectée qu'à l'exécution, voire pas du tout selon le compilateur.
Ce qui se cache derrière println!
Sans entrer dans tous les détails d'implémentation de la bibliothèque standard, on peut voir println! comme une surcouche au-dessus de deux autres macros :
println!("Age = {}", age);
devient conceptuellement :
print!("{}\n", format_args!("Age = {}", age));
Et format_args! est lui-même une macro.
Autrement dit, l'affichage en Rust repose largement sur la génération de code à la compilation.
Pourquoi c'est intéressant pour un développeur C++ ?
Lorsqu'on découvre Rust, on pourrait penser que le point d'exclamation n'est qu'un détail syntaxique.
En réalité, il révèle plusieurs caractéristiques fondamentales du langage :
- Rust privilégie les vérifications à la compilation.
- Les macros sont utilisées pour générer du code sûr et performant.
- Le système de formatage est vérifié avant l'exécution.
- Aucune allocation mémoire inutile n'est nécessaire pour construire la chaîne à afficher.
Pour un développeur C++, cela ressemble souvent à un mélange entre la puissance des templates modernes et la simplicité d'utilisation de printf.
A Retenir
Rust ne possède par de fonctions variadiques et par conséquent pour afficher quelque chose sur la console on passe par une macro reconnaissable par le ! apres le nom.