IANAL (I Am Not A Linguist), mais quand j’entends du québécois, je ne comprends pas tout, mais je comprends. Mais qu’en est-il des dialectes du langage C ? Car oui, le langage C a des dialectes, et nous allons voyager un peu à travers l’un d’entre eux, le dialecte GNU, supporté principalement par GCC, mais aussi, en partie, par Clang.
Si on essaie de compiler le code suivant avec le compilateur Clang en lui demandant d’être pédant :
On obtient un message d’avertissement :
Clang a compris le sens de ce code, mais ce code n’est pas standard. D’ailleurs est-ce que toi, lecteur, tu comprends sa signification ? Si l’argument entre le point interrogation et le point-virgule est omis, il prend pour valeur le résultat de l’évaluation de la condition, dans notre cas arg. En ce sens, cela fournit une syntaxe assez proche de l’opérateur or en Python.
Dans la suite de cet article, nous explorerons plusieurs constructions propres au dialecte GNU C, tel que documenté dans https://gcc.gnu.org/onlinedocs/gcc/C-Extensions.html, en essayant de retrouver à chaque fois une construction similaire dans une version plus moderne du langage, ou dans un autre langage.
1. Statement Expression
En GNU C, la construction ({int result = foo(); result >= 0;}) est une expression dont la valeur est le résultat de l’évaluation de la dernière instruction. Cela permet de construire des macros élégantes avec variables intermédiaires, par exemple :
Cette macro utilise aussi une autre extension GNU, __typeof qui n’est pas sans rappeler le decltype de C++, et qui a été standardisée en C23 sous le nom typeof. À peine commence-t-on notre voyage qu’on est déjà assailli de verbiage inintelligible :-).
On notera que depuis C++17, le programmeur C++ a à sa disposition une construction semblable partout où l’on évalue une condition, par exemple :
2. Computed Goto
D’après le théorème fondamental de l’ingénierie logicielle, il n’est pas de problème qui ne puisse être résolu avec un niveau d’indirection supplémentaire. Si on considère que les goto sont un problème, on ne peut que se réjouir de la possibilité de faire des sauts indirects (possibilité offerte par le jmp de x86, d’ailleurs) en prenant l’adresse d’un label pour ensuite sauter à cette adresse. La syntaxe pour lire l’adresse d’un label est void* ptr = &&label_name, et on peut y sauter à travers goto *ptr. C’est souvent utilisé pour implémenter des automates, chaque état correspondant à un label. Par exemple, le code suivant implémente un switch pour les chaînes de caractères, avec un mix de C++ et de GNU C du plus bel effet :
3. Nested Functions
Définir une fonction à l’intérieur d’une fonction, on connaît, ça existe en C++ depuis C++11, c’est une lambda. Mais avec GNU C, on peut définir une fonction à l’intérieur d’une fonction, elle capture alors toutes les variables de la fonction englobante par référence, et cela permet, au choix de limiter la portée d’une fonction, d’associer un état à une fonction, ou de se tirer une balle dans le pied. Voici un exemple d’usage sympathique :
Le code généré est assez astucieux, il repose sur l’utilisation d’un trampoline pour éviter tout appel de fonction, je laisse les curieux investiguer.
4. La foire à la saucisse des nombres flottants
Vous avez besoin de types de données particulières pour représenter une certaine classe de nombres à virgule ? GNU C est fait pour vous : fractions, nombres décimaux, nombres à virgule flottante sur 80 bits ou 128 bits, description d’un litteral flottant par sa représentation en hexadécimal (syntaxe d’ailleurs reprise en C++17 !). Tout cela est possible, je vous laisse tenter de retrouver le type de chacun des littéraux suivants :
Depuis C++11, il est possible de définir ses propres opérateurs de conversion de littéraux.
5. Le bon vieux struct hack
Le motif suivant n’est pas standard, mais est tellement courant que vous l’avez sûrement déjà rencontré :
Une version de cette extension a été normalisée en C99 sous le nom de « flexible array member » et dans ce cas, la déclaration de data est juste char data[];.
6. Tableau de taille variable
Ce n’est que depuis le C99 que l’on peut déclarer un tableau de taille variable, ce qui a souvent pour effet d’agrandir la pile d’une valeur dynamique (ce qui peut avoir des implications en termes de sécurité, mais passons, nous ne sommes pas dans MISC ici). Par exemple, on peut déclarer un tableau en vue de préparer un strcpy : char buffer[strlen(input) + 1];. Cette syntaxe est invalide en C++ et optionnelle depuis C11, mais ça reste du GNU C valide !
7. Les case range
Il arrive de devoir écrire un switch pour lequel plusieurs cases marquent le même fragment de code, ce qui conduit au relativement disgracieux :
En GNU C, on bénéficie d’un raccourci de langage pour exprimer cela :
Plutôt élégant, non ? Et pourtant, rien de tel en C moderne ni en C++, mais c’est un motif qu’on peut retrouver dans les match de Python.
8. Les attributs
Rien que par leur syntaxe, on sent qu’on est dans du dialecte pur : __attribute__((always_inline)), ce n’est clairement pas du C standard ! Et pourtant, certains de ces attributs sont tellement utiles qu’ils trouvent leur chemin dans de nombreuses bases de code. Et une fois de plus, C++ a fini par normaliser certaines de ces expressions, comme par exemple __attribute__((noreturn)) qui s’exprime avec un joli [[noreturn]] en C++11 (et qui signifie que la fonction ne rend jamais la main, par exemple parce qu’elle appelle systématiquement abort(), ou parce qu’elle lève systématiquement une exception).
Petite sélection du chef :
- __attribute__((malloc)) indique que la valeur de retour de la fonction a les mêmes propriétés que la valeur de retour de malloc, ce qui aide l’analyse d’aliasing.
- __attribute__((flatten)) demande au compilateur d’inliner tous les appels de fonction à l’intérieur de la fonction décorée, ce qui revient à mettre son graphe d’appel à plat.
- __attribute__((deprecated)) est un autre classique (normalisé sous la forme [[deprecated]] en C++14) qui enjoint (oui, qui enjoint) le compilateur à émettre un message d’avertissement si la fonction associée est référencée. Existe aussi sous la forme __attribute__((unavailable)) pour générer une erreur.
- __attribute__((pure)) indique au compilateur que la fonction marquée n’a pas d’effets de bord, ce qui permet au compilateur d’appliquer diverses optimisations (p. ex. élimination d’expressions communes).
- __attribute__((hot)) (et son pendant __attribute__((cold))) permet de se substituer aux informations de profiling si celles-ci ne sont pas présentes pour indiquer au compilateur s’il doit optimiser plus agressivement et placer la fonction dans une section regroupant toutes les fonctions utilisées fréquemment, ou au contraire s’il peut optimiser plus légèrement la fonction et la placer dans une section qui a peu de chances d’être chargée en mémoire (typiquement, une fonction qui affiche un message d’erreur avant d’interrompre le programme).
9. #pragma GCC
Le langage C a ceci d’amusant qu’il a prévu une façon standard d’écrire des dialectes, à travers les directives #pragma. Le résultat est moins divertissant qu’une nouvelle syntaxe, mais au moins c’est portable (dans le sens où un compilateur tiers peut ignorer cette directive sans lâcher une erreur de syntaxe). On y retrouve des classiques comme #pragma GCC unroll n pour dérouler une boucle d’un pas de n, mais aussi le support de directives venant d’autres compilateurs, comme #pragma push_macro("macro_name") qui nous vient de MSVC et qui permet avec son compagnon #pragma pop_macro("macro_name") de gérer une pile de définitions de macros. J’utilise personnellement de temps en temps #pragma GCC optimize (string...) pour changer le niveau d’optimisation d’une fonction, très pratique quand on cherche à isoler une régression du compilateur !
10. [[noreturn]] void terminate() noexcept
Au-delà de la gratification intellectuelle de découvrir de nouvelles constructions syntaxiques — et nous n’avons fait qu’effleurer le sujet, cet article avait pour but d’illustrer deux aspects, qui sont d’ailleurs communs aux langages informatiques et naturels : quand on cherche à exprimer quelque chose, on crée le vocabulaire pour le faire, quelle que soit la forme pure du langage (-pedantic disait le compilateur). Et de manière amusante, cet usage rentre dans les mœurs pour finir par se standardiser :-).
Je tiens par ailleurs à remercier Lancelot Six pour sa relecture aussi agréable que précise de cet article.