Crévindiou, c’est pas du bon C d’chez nous ça, cé du C deu’l ville !

Magazine
Marque
GNU/Linux Magazine
Numéro
267
Mois de parution
janvier 2024
Spécialité(s)


Résumé

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.


Body

Si on essaie de compiler le code suivant avec le compilateur Clang en lui demandant d’être pédant :

void foo(int arg) {
    return arg ? : -1;
}

On obtient un message d’avertissement :

$ clang -c a.c -fpedantic
a.c:2:16: warning: use of GNU ?: conditional expression extension, omitting middle operand [-Wgnu-conditional-omitted-operand]

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 :

#define max(x, y) ({ __typeof(x) tmp_x = x; __typeof(y) tmp_y = y; tmp_x > tmp_y ? tmp_x : tmp_y; })

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 :

if (auto res = foo(); res >= 0) {
    // stuff
}

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 :

void foo(std::string state) {
    static const std::unordered_map<std::string, void*> jumps = {
      {"case0", &&case0},
      {"case1", &&case1},
      {"case2", &&case1},
    };
    auto where = jumps.find(state);
    if(where == jumps.end()) return;
    goto *where->second;
 
  case0:
    // stuff
    return;
  case1:
    // other stuff
    return;
  case2:
    // yet another set of stuff
    return;
}

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 :

void my_sort(float *data, size_t n, bool reverse)
{
    int cmp(const void *a, const void *b)
    {
        int r = *(float *)a > *(float *)b;
        return reverse ? -r : r;
    }
    qsort(data, n, sizeof(*data), cmp);
}

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 :

12.3W; /* non ce ne sont pas des Watts */
12.3q;
12.3df;
12.3dd;
12.3dl; /* pas plus que des décilitres ici */;
0x1.fp3; /* le prédécesseur du mp3 ? */

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é :

struct array {
    int size;
    char data[0];
};
struct array * myarray = malloc(sizeof(struct array) + my_size);
myarray->size = my_size;

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 :

case 0:
case 1:
case 2:
case 3:
   // stuff

En GNU C, on bénéficie d’un raccourci de langage pour exprimer cela :

case 0 .. 3:
  // stuff

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.



Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous