Pourquoi j’aime le C
Coder en C. En 2020 ? Vraiment ?
Retour en arrière
Tout d’abord, il faut savoir que le C est le premier langage que j’ai appris. Cela remonte aux alentours de 2006. À cette époque là j’avais déjà 4 ans d’expérience sous Linux et je souhaitais commencer à développer car passionné des logiciels libres et des jeux, je voulais aussi faire mes propres créations.
C’est alors en discutant avec mes communautés de l’époque sur IRC que je demandais l’avis d’un premier langage à apprendre. Certains me disaient Python, d’autres Ruby et beaucoup du C étant sur des communauté linuxiennes et les systèmes UNIX forment principalement l’origine du C.
Je décide de jeter un œil au Ruby et commence à lire la documentation officielle. N’ayant absolument aucune formation en développement à ce moment j’avais vraiment du mal à comprendre pas mal de concepts et Ruby n’est pas si simple pour un débutant je trouve. Je finis par jeter l’éponge car le langage ne m’a pas attiré plus que ça finalement.
Sans vraiment comprendre pourquoi j’avais cette attirance indescriptible pour le C, j’avais envie de le maitriser car c’était le langage de la base d’UNIX et je décide donc de m’y lancer corps et âme. Pour ce faire j’emprunte la bible K&R à la bibliothèque et me met à le lire en même temps que je développe et teste des trucs.
Pour m’aider à avancer et à m’inspirer je regarde des programmes libres que j’aime bien et qui sont simples afin de voir comment eux font. D’abord je développe un petit parseur d’options en format .ini, ensuite un puissance 4 en ligne de commande et enfin un snake. Je développe aussi mon premier jeu avec SDL, un tetris (aujourd’hui défunt).
Mais en C, tout est minimaliste et le moindre projet requiert souvent de recoder des fonctionnalités de base comme des chaînes de caractères, des tableaux dynamiques, etc… Alors je commence à réfléchir à un autre langage plus moderne et entendant parler de la norme du C++11, je décide de m’y lancer en 2010. Je dois avouer que si cette norme n’avait pas existé je n’aurais sans doute jamais commencé ce langage.
Les langages « modernes »
Ce qui me dérange avec les langages modernes.
C++
Ayant commencé le C++ en même temps que la norme C++11, j’ai été séduit par la manière de développer avec les nouvelles fonctionnalités. J’ai alors commencé à suivre chaque nouvelle version, le C++14, le C++17 et le C++20.
C’est avec le C++20 que j’ai commencé à me poser des questions. Le langage devient tellement difficile qu’il est presque impossible de le connaître intégralement. De plus, le langage étant tellement compliqué il donne moins envie à un néophyte de le commencer.
Les concepts (C++20)
Les concepts sont une manière de contractualiser les paramètres templates en leur demandant d’avoir certaines caractéristiques (une fonction présente, un champ présent, être copiable, etc…). Cela fait depuis les débuts du C++ qu’on travaille sur le sujet mais c’est finalement présent en C++20 et sa syntaxe est compliquée.
template<typename T>
concept Hashable = requires(T a) {
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
Boost
Quand vous développez en C++, à moins de ne pas avoir besoin de trop de fonctionnalités, vous êtes quand même vite intéressé d’utiliser Boost car c’est celle qu’on vous conseillera le plus souvent. Elle a la notoriété d’être l’origine de pas mal de fonctionnalités présentes dans la bibliothèque standard du C++ mais expérimentée d’abord chez Boost.
Boost est un agglomérat d’une quantité énorme de modules à tout faire. Il est souligné que c’est une bibliothèque ”headers only” et que chaque module est quasiment indépendant. Dans les deux cas c’est faux. D’abord, beaucoup de modules ont besoin Boost.System qui n’est pas ”headers only” et la plupart des modules utilisent d’autres ce qui rend très difficile un choix individuel.
De plus, c’est une bibliothèque extrêmement complexe où
l’overengineering est particulièrement présent. La bibliothèque Boost.Process
utilise par exemple l’operator <
et
l’operator >
pour définir des arguments spécifiques à
une fonction.
Quand vous êtes fan d’UNIX et du shell ça parait effectivement élégant à l’utilisation :
::system("c++ main.cpp", bp::std_out > stdout, bp::std_err > stderr, bp::std_in < stdin); bp
Allez expliquer au débutant pourquoi on utilise une comparaison en argument, c’est illogique et détourné de la fonction de base ce qui rend la documentation et la compréhension encore plus complexe.
Les modules Boost.Asio, Boost.Log sont aussi particulièrement compliqués.
En soit, je ne décourage pas l’utilisation de Boost, mais je dirais qu’il faut bien choisir et regarder chacun d’entre eux.
Bibliothèques écrites en C++98
Le C++ étant un langage qui évolue, il n’est pas rare de voir des bibliothèques écrites bien avant les dernières normes. Et dans mon expérience, presque à chaque fois où j’ai essayé des bibliothèques externes j’ai été confronté à ce problème.
- Non utilisation des smart pointers, la bibliothèque continue
d’utiliser
new
etdelete
manuellement ; - Utilisaton de
void *
et pointeurs de fonction comme en C (au lieu de std::function) ; - Absence de ”move semantics” rendant difficile le déplacement des objets quand nécessaire ;
- Utilisation de
typedef
au lieu des aliasusing
.
Cela me faisait perdre du temps, j’hésitais entre contribuer à la bibliothèque et/ou chercher une alternative plus moderne.
Le support des compilateurs
Le C++ étant uniquement une norme, c’est aux développeur de compilateurs et bibliothèques d’implémenter ce qui est spécifié dans le standard. À chaque nouvelle norme, vous devez donc vérifier que vous pouvez utiliser certaines fonctionnalités ou non dans votre projet selon la plateforme cible.
Visual Studio a par exemple longtemps mal supporté le C++11 ce qui rendait le développement sous Windows particulièrement fastidieux.
Rust
Avec un désarroi grandissant en C++, je commence à regarder de nouvelles alternatives. Depuis plusieurs années Rust est un langage proche du C++ avec la complexité en moins, la sureté en plus. Que demander de plus ?
En suivant le langage de loin, je regarde comment il évolue ainsi que les avis des différents développeurs. Je suis attiré sur plein de points fondamentaux du Rust mais la syntaxe je n’accroche pas. J’essaye quand même de lire le livre officiel mais rien y fait, ce langage ne m’attire pas. Et pour cause.
Crates
Rust vient avec son propre gestionnaire de dépendances officiel, cargo. En soit c’est plutôt bien car ça permet de développer en visant des bibliothèques précises et en s’affranchissant de l’installation des bibliothèques.
Premier problème, le sites crates.io vous force à utiliser GitHub. Ça commence mal car j’utilise Mercurial et je ne souhaite pas utiliser mon compte GitHub si je faisais des paquets Rust. De ce que je lis, les paquets n’ont pas nécessairement besoin d’un SCM particulier mais pour les publier sur le référentiel il est tout de même indispensable d’avoir un compte GitHub. Soupir.
Deuxième problème, les paquets Rust sont conçus comme les paquets de NPM : de petits modules minimalistes pour faire une minuscule chose. Prenons comme exemple exa, c’est un petit exécutable en ligne de commande permettant d’afficher le contenu d’un répertoire. Un remplacement « moderne » de ls.
Dans le fond, je me demande ce qui est réellement manquant à la
commande POSIX ls
mais soit. Je regarde le projet. À
l’heure où j’écris ces lignes, le projet a besoin de 15
dépendances directes pour être construit ce qui me pousse à penser en
quoi une simple commande pour lister des répertoires a-t-elle autant
besoin de dépendances ? Mais ce n’est pas tout, chacune de ces
dépendances en a aussi, au total il faut installer environ
45 dépendances au total. À ce stade, je me demande où
est passée l’élégance et la simplicité. Surtout quand il s’agit d’une
commande pour lister un répertoire.
Go
Pourquoi pas mais la présence d’un ramasse miettes me dérange. J’ai du mal à saisir qu’on puisse encore développer des langages en ayant besoin. Rust a très bien réussi à s’en passer et C++ aussi.
Ce qui me plait dans le Go : l’extrême cohérence inhérente au langage. Pour exporter une fonction elle doit être en majuscule sinon elle reste privée. Ce langage utilise fortement la notion de convention plutôt que configuration ce qui fait du code très homogène sans trop de personnalisation.
Que me reste t-il ?
J’ai toujours préféré les langages natifs. J’aime le fait de compiler une application et que celle ci n’ait ni besoin d’une machine virtuelle, ni besoin d’un interpréteur, ni besoin d’une complexité de déploiement.
Swift
Pourquoi pas, le langage est propre. Très bien conçu et facile d’utilisation. Cependant il est très centré autour de macOS et les API graphiques comme SwiftUI ne sont pas disponibles sur les autres systèmes.
Je pense que si je devais développer une application portable avec interface graphique je la ferai sans doute en C et les interfaces graphiques dans la boîte à outil de la plateforme cible (Swift pour macOS, Gtk pour Linux, etc…).
Ce que j’aime en Swift :
- Langage simple et pourtant natif ;
- Fonctionnalités intéressantes ;
- Convivialité d’écriture ;
- Documentation exemplaire.
C
C’est un langage qui ne disparaitra sans doute jamais car il est nécessaire et fait les choses simples. Le C reste un langage de choix dans l’embarqué et les systèmes d’exploitation car on sait exactement ce que fait chaque fonction et il n’y a pas de boite noire.
Ce que j’aime le plus en C :
La simplicité
En C, rien est compliqué. C’est un langage qu’on peut apprendre en quelques jours et maitriser en quelques semaines. On ne passe pas non plus des heures à architecturer son projet ou sa bibliothèque car en C il n’y a que des fonctions et des types. Pas de templates à foison, pas de polymorphisme à outrance, juste des fonctions qui font les choses bien.
Le temps de compilation
C’est un peu anecdotique, mais c’est plaisant de voir le temps de compilation d’un projet en C et un en C++ (comme WebkitGtk).
À l’heure actuelle, compiler molko (avec la documentation doxygen, les ressources, les tests et les exemples) et ses 7000 lignes de code prend environ 30 secondes sur mon MacBook Pro.
$ time make >/dev/null
make > /dev/null 27.58s user 8.53s system 149% cpu 24.218 total
La facilité à implémenter et documenter
Quand vous développez en C, vous savez exactement quels symboles seront présents dans votre exécutable final. En plus, si vous découpez convenablement votre bibliothèque vous pouvez faire des économies sur l’exécutable final.
Bonus, beaucoup de fonctions C sont documentées en page de manuel et quand vous êtes un fan d’UNIX c’est le bonheur.
La simplicité à construire sur un système POSIX
Avec un environnement POSIX il est encore plus facile de développer en C. Pour rappel POSIX est une norme implémentée par un grand nombre de système d’exploitation (à l’exception de Windows).
vim foo.c
make foo
Littéralement. Et il n’y a pas besoin de Makefile, POSIX définit des règles par défaut. Évidemment, si vous découpez votre programme en plusieurs fichiers il faudra en écrire un, mais il reste tout aussi simple :
# Mon minimaliste makefile
SRCS= foo1.c foo2.c foo3.c
OBJS= $(SRCS:.c=.o)
foo: $(OBJS)
$(CC) $(CFLAGS) -o $@ $(OBJS) $(LDFLAGS)
Et puis
make foo
La rigueur
En C, on a pas de bibliothèques à tout faire de base comme des tableaux dynamiques, des chaînes de caractères dynamiques et autre. Du coup on a plusieurs possibilités :
- Architecturer d’abord les données et ensuite l’algorithme ;
- Utiliser une bibliothèque à tout faire (glib, apr, etc…).
Pour ma part, j’essaye d’abord la solution 1. Par exemple, dans le développement de mon jeu Molko je sais à l’avance que le nombre maximum de sorts que je vais autoriser à un joueur est connu à l’avance, je peux alors utiliser un tableau fixe.
Aussi, pour rendre l’API la plus simple et stupide possible je laisse le maximum de logique à l’utilisateur. Ainsi au lieu de gérer la mémoire côté API je la laisse au développeur en indiquant bien dans la documentation que pour fonctionner, cet objet doit rester valide.
Ce qui – dans mon cas – ressemble à ça :
/* Une texture qu'on peut dessiner à l'écran */
struct texture monimage;
/* Un objet qui dessine une partie précise d'une texture. */
struct sprite masprite;
/* Je charge mon image. */
(&monimage, "grid.png");
image_open
/* J'initialise ma sprite qui a des cellules de 64x64 pixels. */
(&masprite, &monimage, 64, 64);
sprite_init
/* Je dessine la ligne 0 et la colonne 3 en x=10 et y=10. */
(&masprite, 0, 2, 10, 10); sprite_draw
Ici, mon objet sprite
référence uniquement ma texture.
C’est bien à l’utilisateur de s’assurer que l’objet texture
doit rester valide jusqu’à ce que l’objet sprite
ne soit
plus utilisé.
Ce qui me dérange toujours en C
Il y a toujours des choses qui sont pénibles au quotidien :
- L’absence de convention : CamelCase, underscore_case,
typedef
ou non ; - Le mauvais support du C11 ;
- Les erreurs classiques des variables non initialisées ;
- L’absence de namespaces.
Peut on tout faire en C ?
Oui et non.
Il est tout à fait possible de coder un jeu en C comme il est possible de créer un site web en C. Cela ne veut pas dire qu’il faut tout coder en C. Il y a des tâches qui sont bien plus faciles à faire dans d’autres langages. Si votre but est d’analyser un fichier CSV ou correctement formaté l’utilisation de AWK ou un langage plus haut niveau comme Python vous sera bien plus facile.
Concernant les jeux vidéos en 3D, le C++ reste la domination absolue. Les moteurs 3D sont souvent en C++ (ou en C# ces derniers temps) et les rares en C sont plutôt vieux. Il n’empêche que chez Cryptic Studios il est dit qu’ils développent tous leurs jeux en C, dommage qu’ils ne soient pas opensource.
Redévelopper les projets en C ?
Il est vrai que si vous avez suivi les dépôts Mercurial, vous avez pu constater que paster a d’abord été écrit en C++ puis est repassé en C. C’était principalement pour une raison : la bibliothèque Boost.Beast est puissante mais pénible à utiliser et je voulais absolument le coder en CGI. Aussi, je me suis lancé le défi de voir si c’était possible et compliqué finalement j’étais content du résultat.
En revanche, il est peu probable que je redéveloppe irccd car celui ci a maintenant atteint une maturité et stabilité que j’apprécie. Ce n’est pas non plus un projet ayant beaucoup de contributions et une grande base utilisateur, il n’y aurait aucun intérêt à le redévelopper en C si ce n’est de retirer la dépendance à Boost.
Est-il si difficile de développer en C ?
La plupart des critiques du C citent l’insécurité du langage. En effet en C il est vraiment très facile de se planter. En écrivant hors bornes, en utilisant une variable non initialisée, en faisant un double-free, etc…
Mais avec le temps les développeurs des compilateurs ont pu améliorer
le développement. Avec clang on peut utiliser l’option
-fsanitize
pour analyser en cours de route les erreurs. Je
pense que ça devrait être par défaut lors du développement.
Conclusion
Je pense pas que le C va mourir. C’est un langage qui est un peu moins utilisé pour des nouveaux projets mais il continue d’avoir une base d’utilisateurs passionnés par ce langage qui apprécient sa simplicité.
Exemples de projets plus ou moins récents en C :