GCC 15 en C23 par défaut : ce qui casse et comment corriger

GCC 15 compile en C23 par défaut : typedef int bool, déclarations f() et pointeurs de fonction sans paramètres ne passent plus. Chaque erreur et sa correction.

Par
Publié le

Le projet compilait sans un warning. Après une mise à jour de GCC, il s'arrête sur 'bool' cannot be defined via 'typedef' ou sur too many arguments to function, dans des fichiers que personne n'a touchés depuis des mois. Ton code est le même ; GCC, lui, applique désormais par défaut une autre version du langage.

En bref

Depuis GCC 15 (15.1 est sorti le 25 avril 2025), le C se compile par défaut en -std=gnu23 au lieu de -std=gnu17. Trois changements du C23 cassent du code ancien : bool, true et false deviennent des mots-clés, donc typedef int bool; est refusé ; une déclaration int f(); signifie désormais « aucun paramètre », donc f(1) donne too many arguments to function ; et un pointeur de fonction déclaré void (*cb)() n'accepte plus une fonction qui prend des paramètres. La correction est d'écrire des prototypes complets et d'utiliser le bool du langage ; -std=gnu17 rend l'ancien comportement en attendant.

Quelle version du C ton compilateur utilise-t-il ?

La macro __STDC_VERSION__ répond. Le compilateur la définit avec une valeur différente pour chaque version de la norme :

version.c
#include <stdio.h>
int main(void){    printf("%ld\n", __STDC_VERSION__);    return 0;}
Terminal
$ gcc --version | head -1gcc (GCC) 15.3.0$ gcc version.c -o version && ./version202311$ gcc -std=gnu17 version.c -o version && ./version201710

202311 désigne C23, la norme ISO/IEC 9899 publiée fin 2024. Avec -std=gnu17, on retrouve 201710, la valeur de C17. Parmi les valeurs plus anciennes : 199901 pour C99, 201112 pour C11. Le terminal des exercices Grademe, sous Alpine avec GCC 15.2, affiche lui aussi 202311 sans option.

Pour vérifier sans écrire de programme, demande au préprocesseur la liste de ses macros :

Terminal
$ gcc -dM -E -x c /dev/null | grep STDC_VERSION#define __STDC_VERSION__ 202311L

Le préfixe gnu de gnu23 signifie « C23 plus les extensions de GCC », par opposition à -std=c23, plus strict (la fiche gcc(1) résume les options -std=). Les trois changements de la suite viennent de C23 lui-même : ils cassent dans les deux modes.

'bool' cannot be defined via 'typedef'

Avant C99, le C n'avait pas de type booléen, et beaucoup de projets définissaient le leur. C99 a ajouté _Bool et l'en-tête <stdbool.h>, qui fournissait bool, true et false sous forme de macros. C23 en fait de vrais mots-clés, au même titre que int. On ne peut donc plus les redéfinir :

booleen.cÀ éviter
#include <stdio.h>
typedef int bool;#define true 1#define false 0
int main(void){    bool fini = false;
    printf("%d\n", fini);    return 0;}
Terminal
$ gcc -Wall -Wextra booleen.c -o booleenbooleen.c:3:13: error: 'bool' cannot be defined via 'typedef'    3 | typedef int bool;      |             ^~~~booleen.c:3:13: note: 'bool' is a keyword with '-std=c23' onwardsbooleen.c:3:1: warning: useless type name in empty declaration    3 | typedef int bool;      | ^~~~~~~

L'autre forme qu'on croise dans les vieux projets, une énumération, échoue un cran plus tôt :

Terminal
$ gcc -Wall -Wextra enumbool.c -o enumboolenumbool.c:3:16: error: cannot use keyword 'false' as enumeration constant    3 | typedef enum { false, true } bool;      |                ^~~~~enumbool.c:3:16: note: 'false' is a keyword with '-std=c23' onwardsenumbool.c:3:30: error: expected ';', identifier or '(' before 'bool'    3 | typedef enum { false, true } bool;      |                              ^~~~enumbool.c:3:30: warning: useless type name in empty declaration

La correction tient en une ligne : supprime ta définition et inclus <stdbool.h>. En C23 l'en-tête ne sert plus à rien, mais il ne gêne pas, et le même fichier compile alors en C99, en C17 et en C23 :

Définition maisonÀ éviter
#include <stdio.h>
typedef int bool;#define true 1#define false 0
Le bool du langageCorrect
#include <stdbool.h>#include <stdio.h>
Terminal
$ gcc -Wall -Wextra booleen_ok.c -o booleen_ok && ./booleen_ok0$ gcc -std=c99 -Wall -Wextra booleen_ok.c -o booleen_ok && ./booleen_ok0

Une précaution que donne la documentation de GCC : bool et int ne sont pas le même type au niveau de l'ABI, la convention qui fixe la taille et le passage des données entre fichiers compilés. Si ton bool maison traversait une frontière binaire, une structure écrite dans un fichier ou une bibliothèque déjà compilée, vérifie que le changement de type ne modifie pas la taille ou la disposition des données.

bool, true et false ne sont pas les seuls nouveaux mots-clés. C23 réserve aussi alignas, alignof, constexpr, nullptr, static_assert, thread_local, typeof et typeof_unqual. Une variable ou une fonction qui porte un de ces noms ne compile plus.

too many arguments to function : la fin des déclarations f()

Jusqu'à C17, int carre(); déclarait une fonction dont on ne précisait pas les paramètres : le compilateur acceptait n'importe quel appel sans rien vérifier. En C23, des parenthèses vides veulent dire « aucun paramètre », exactement comme (void). Un appel avec un argument devient une erreur :

vide.cÀ éviter
#include <stdio.h>
int carre();
int main(void){    printf("%d\n", carre(7));    return 0;}
int carre(int n){    return n * n;}
Terminal
$ gcc -Wall -Wextra vide.c -o videvide.c: In function 'main':vide.c:7:20: error: too many arguments to function 'carre'; expected 0, have 1    7 |     printf("%d\n", carre(7));      |                    ^~~~~ ~vide.c:3:5: note: declared here    3 | int carre();      |     ^~~~~vide.c: At top level:vide.c:11:5: error: conflicting types for 'carre'; have 'int(int)'   11 | int carre(int n)      |     ^~~~~vide.c:3:5: note: previous declaration of 'carre' with type 'int(void)'    3 | int carre();      |     ^~~~~

La dernière note dit tout : int carre(); a désormais le type int(void). La correction est d'écrire le prototype complet, int carre(int n);, ce qui était déjà la bonne pratique depuis C89 :

Terminal
$ gcc -Wall -Wextra vide_ok.c -o vide_ok && ./vide_ok49

Ce genre de déclaration se cache souvent dans de vieux en-têtes, du type char *strstr(); ou void *malloc(); écrits à la main au lieu d'inclure l'en-tête standard. Dans ce cas, supprime la ligne et inclus le bon en-tête. Les erreurs sur les appels sans déclaration du tout, elles, datent de GCC 14 : elles sont expliquées dans implicit declaration of function.

Les pointeurs de fonction déclarés avec ()

La même règle s'applique aux pointeurs de fonction. void (*rappel)() désignait « un pointeur vers une fonction qui renvoie void », quels que soient ses paramètres. En C23, c'est un pointeur vers une fonction sans paramètre, incompatible avec une fonction qui en attend un :

rappel.cÀ éviter
#include <stdio.h>
void dire(const char *mot){    printf("%s\n", mot);}
int main(void){    void (*rappel)() = dire;
    rappel("bonjour");    return 0;}
Terminal
$ gcc -Wall -Wextra rappel.c -o rappelrappel.c: In function 'main':rappel.c:10:24: error: initialization of 'void (*)(void)' from incompatible pointer type 'void (*)(const char *)' [-Wincompatible-pointer-types]   10 |     void (*rappel)() = dire;      |                        ^~~~rappel.c:3:6: note: 'dire' declared here    3 | void dire(const char *mot)      |      ^~~~rappel.c:12:5: error: too many arguments to function 'rappel'; expected 0, have 1   12 |     rappel("bonjour");      |     ^~~~~~ ~~~~~~~~~

Deux erreurs pour une seule ligne fautive : l'initialisation, puis l'appel. La correction est d'écrire le type complet du pointeur, void (*rappel)(const char *) = dire;.

Un cas classique, que cite la documentation de GCC, est le gestionnaire de signal. signal() renvoie l'ancien gestionnaire, une fonction qui reçoit le numéro du signal :

sig.cÀ éviter
#include <signal.h>#include <stdio.h>
int main(void){    void (*ancien)();
    ancien = signal(SIGINT, SIG_IGN);    printf("%s\n", ancien == SIG_DFL ? "SIG_DFL" : "autre");    return 0;}
Terminal
$ gcc -Wall -Wextra sig.c -o sigsig.c: In function 'main':sig.c:8:12: error: assignment to 'void (*)(void)' from incompatible pointer type '__sighandler_t' {aka 'void (*)(int)'} [-Wincompatible-pointer-types]    8 |     ancien = signal(SIGINT, SIG_IGN);      |            ^In file included from sig.c:1:/usr/include/signal.h:72:16: note: '__sighandler_t' declared here   72 | typedef void (*__sighandler_t) (int);      |                ^~~~~~~~~~~~~~sig.c:9:27: warning: comparison of distinct pointer types lacks a cast [-Wcompare-distinct-pointer-types]    9 |     printf("%s\n", ancien == SIG_DFL ? "SIG_DFL" : "autre");      |                           ^~

La note de GCC donne le type attendu : void (*)(int). Déclare void (*ancien)(int); et le programme compile et affiche SIG_DFL. Le message cite /usr/include/signal.h, l'en-tête de la glibc ; avec une autre bibliothèque C, le nom du type change, pas la correction.

C

list_apply

list_apply parcourt une liste chaînée et appelle, sur la donnée de chaque nœud, une fonction reçue en paramètre. Son prototype impose void (*f)(void *) : l'occasion d'écrire le type complet d'un pointeur de fonction et de s'en servir.

Ouvrir l'exercice

Et les définitions à l'ancienne, façon K&R ?

C23 supprime aussi les définitions de fonction à l'ancienne, où les types des paramètres sont donnés entre la parenthèse et l'accolade. GCC 15 les accepte encore en mode par défaut, avec un warning affiché même sans option (il se taisait en gnu17). Il ne les refuse qu'en mode strict :

kr.cÀ éviter
#include <stdio.h>
int somme(a, b)    int a;    int b;{    return a + b;}
int main(void){    printf("%d\n", somme(2, 3));    return 0;}
Terminal
$ gcc -Wall -Wextra kr.c -o krkr.c: In function 'somme':kr.c:3:5: warning: old-style function definition [-Wold-style-definition]    3 | int somme(a, b)      |     ^~~~~$ gcc -std=c23 -pedantic-errors kr.c -o krkr.c: In function 'somme':kr.c:3:5: error: old-style function definition [-Wold-style-definition]    3 | int somme(a, b)      |     ^~~~~

Rien ne bloque donc la compilation, mais ce code n'est plus du C23 valide : réécris-le en int somme(int a, int b).

Avec -Werror, deux warnings apparus avec GCC 15

Si ton projet compile avec -Werror, deux warnings arrivés avec GCC 15 peuvent aussi bloquer la construction. Le premier, -Wunterminated-string-initialization, activé par -Wextra, signale un tableau de char exactement aussi long que la chaîne qui l'initialise, donc sans place pour le '\0' final :

Terminal
$ gcc -Wall -Wextra code.c -o code && ./codecode.c: In function 'main':code.c:5:20: warning: initializer-string for array of 'char' truncates NUL terminator but destination lacks 'nonstring' attribute (5 chars into 4 available) [-Wunterminated-string-initialization]    5 |     char code[4] = "ABCD";      |                    ^~~~~~ABCD

La norme autorise cette écriture, mais code n'est pas une chaîne : strlen ou printf("%s") liraient au-delà du tableau. Si c'est une chaîne, écris char code[] = "ABCD"; et laisse le compilateur compter le '\0'. Si ce sont quatre octets voulus sans terminateur, l'attribut nonstring que cite le message, __attribute__((nonstring)), fait taire le warning. L'article sur les chaînes de caractères en C revient sur ce '\0'.

Le second, -Wheader-guard, activé par -Wall, attrape une faute de frappe dans une garde d'inclusion :

Terminal
$ gcc -Wall -Wextra -c garde.cIn file included from garde.c:1:garde.h:1: warning: header guard 'GARDE_H' followed by '#define' of a different macro [-Wheader-guard]    1 | #ifndef GARDE_Hgarde.h:2: note: 'GARDE_HH' is defined here; did you mean 'GARDE_H'?    2 | #define GARDE_HH

GARDE_H n'étant jamais défini, la garde ne protégeait rien. Celui-là, mieux vaut le corriger que le désactiver.

Solution temporaire : -std=gnu17

Quand tu ne peux pas corriger tout de suite, par exemple dans une bibliothèque tierce, -std=gnu17 rend le mode par défaut de GCC 14. Les trois fichiers refusés plus haut compilent et s'exécutent :

Terminal
$ gcc -std=gnu17 -Wall -Wextra booleen.c -o booleen && ./booleen0$ gcc -std=gnu17 -Wall -Wextra vide.c -o vide && ./vide49$ gcc -std=gnu17 -Wall -Wextra rappel.c -o rappel && ./rappelbonjour

Mets l'option dans les CFLAGS de ton Makefile plutôt que de compter sur la mémoire de chacun. Le revers : en gnu17, tu perds C23, à commencer par nullptr.

Terminal
$ gcc -std=gnu17 -Wall -Wextra nul.c -o nulnul.c: In function 'main':nul.c:5:14: error: 'nullptr' undeclared (first use in this function)    5 |     int *p = nullptr;      |              ^~~~~~~nul.c:5:14: note: each undeclared identifier is reported only once for each function it appears in

Pour migrer un projet pour de bon, une méthode en trois temps :

  1. Compile d'abord en -std=gnu17 -Wall -Wextra -Wstrict-prototypes. Ce dernier warning, function declaration isn't a prototype, désigne chaque déclaration f() qui changera de sens en C23.

  2. Remplace chaque () par la liste des paramètres, ou par (void) si la fonction n'en prend vraiment aucun, et chaque bool maison par <stdbool.h>.

  3. Retire -std=gnu17 et recompile avec GCC 15 : ce qui reste à corriger, ce sont les noms qui entrent en collision avec les nouveaux mots-clés.

Voici le premier point en action, sur le vide.c de tout à l'heure :

Terminal
$ gcc -std=gnu17 -Wall -Wextra -Wstrict-prototypes -c vide.cvide.c:3:1: warning: function declaration isn't a prototype [-Wstrict-prototypes]    3 | int carre();      | ^~~

Les ajouts de C23, de nullptr à constexpr en passant par typeof et #embed, sont présentés avec des exemples dans les nouveautés de C23.

Questions fréquentes

Faut-il encore inclure <stdbool.h> ?

En C23, non : bool, true et false sont des mots-clés. Mais l'inclure ne coûte rien et garde ton code compilable par un compilateur réglé sur C99 ou C17. Pour un projet qui doit compiler partout, garde-le.

Mon code compile chez moi mais pas chez un camarade. Pourquoi ?

Si ton code utilise C23 (nullptr, bool sans en-tête, constexpr) et que ton camarade a un GCC antérieur à la version 15, son compilateur travaille par défaut en gnu17, ou dans un mode plus ancien encore. Ajoute -std=c23 à la commande : l'option existe depuis GCC 14 (avant, elle s'appelait -std=c2x). Dans l'autre sens, un vieux code peut compiler chez lui et casser chez toi avec GCC 15.

-std=c23 ou -std=gnu23 ?

-std=gnu23 est le défaut de GCC 15 et active toutes les extensions de GCC. -std=c23 ne coupe que celles qui contredisent la norme : les intervalles dans un case, par exemple, passent encore sans un mot. Pour apprendre le langage tel qu'il est défini, -std=c23 -Wpedantic signale aussi ces extensions.

Comment savoir quelle norme une option -std= active ?

Affiche __STDC_VERSION__ : gcc -std=gnu17 -dM -E -x c /dev/null | grep STDC_VERSION répond 201710L. Sans option, GCC 15 répond 202311L.

Sources

À toi d'écrire le code.