Nouveautés de C23 : nullptr, constexpr, #embed par l'exemple
nullptr, constexpr, typeof, auto, #embed, ckd_add, <stdbit.h>, %b : chaque nouveauté de C23 avec un exemple compilé par GCC 15 et la version qui la gère.
Depuis GCC 15, le compilateur applique la norme C23 par défaut, sans aucune option. Une partie de ce qu'elle change casse de vieux programmes, c'est le sujet de GCC 15 compile en C23 par défaut. L'autre partie, plus agréable, ajoute des choses qu'on attendait depuis longtemps : un vrai pointeur nul, de vraies constantes, l'inférence de type, des calculs d'entiers vérifiés. Chacune est montrée avec un programme compilé par GCC 15.3 et ce qu'il affiche.
En bref
C23 est la version du langage C publiée par l'ISO fin 2024. Elle ajoute nullptr, fait de bool, true et false des mots-clés, introduit constexpr, typeof et auto pour les variables, les attributs [[nodiscard]] ou [[deprecated]], les littéraux binaires 0b1010 avec le séparateur ', la directive #embed, les calculs vérifiés de <stdckdint.h> et les fonctions de bits de <stdbit.h>. Avec GCC 15, tout ce qui dépend du compilateur est actif par défaut ; GCC 13 et 14 en gèrent une grande partie avec -std=c2x ou -std=c23, et quelques ajouts dépendent de la version de ta bibliothèque C.
Quelle version du C ton compilateur applique-t-il ?
La macro __STDC_VERSION__ répond. Elle vaut 202311L en C23.
#include <stdio.h>
int main(void){ printf("%ld\n", __STDC_VERSION__); return 0;}$ gcc --version | head -n 1gcc (GCC) 15.3.0$ gcc version.c -o version && ./version202311Sans option, GCC 15 compile en -std=gnu23, le C23 plus les extensions GNU ; -std=c23 désactive celles qui contredisent la norme. Sauf mention contraire, les exemples qui suivent sont compilés sans option -std.
nullptr, bool, true et false deviennent des mots-clés
nullptr est une constante de pointeur nul qui a son propre type, nullptr_t (défini dans <stddef.h>). La différence avec NULL est que NULL est une macro dont la définition dépend de l'implémentation : selon le système, ce peut être l'entier 0 ou ((void *)0). _Generic, qui choisit une branche selon le type d'une expression, le montre :
#include <stddef.h>#include <stdio.h>
#define TYPE(x) _Generic((x), nullptr_t: "nullptr_t", void *: "void *", \ bool: "bool", int: "int", default: "autre")
int main(void){ int *p = nullptr;
printf("NULL : %s\n", TYPE(NULL)); printf("nullptr : %s\n", TYPE(nullptr)); printf("true : %s\n", TYPE(true)); printf("p == nullptr : %d\n", p == nullptr); return 0;}$ gcc -Wall -Wextra nullptr.c -o nullptr && ./nullptrNULL : void *nullptr : nullptr_ttrue : boolp == nullptr : 1Avec glibc, NULL est un void * ; nullptr, lui, a le même type partout. Remarque aussi les deux autres lignes : bool s'utilise sans #include <stdbool.h>, et true est de type bool, et non plus un int qui vaut 1. Comme bool est désormais un mot-clé, un vieux typedef int bool; ne compile plus.
constexpr : de vraies constantes de compilation
Une variable const en C n'est pas une constante pour le compilateur : on ne peut pas s'en servir comme taille d'un tableau global.
$ gcc -c const-taille.cconst-taille.c:2:5: error: variably modified 'notes' at file scope 2 | int notes[TAILLE]; | ^~~~~const-taille.c ne contient que const int TAILLE = 4; puis cette ligne. constexpr déclare un objet dont la valeur est calculée et vérifiée à la compilation, et qui peut servir partout où une constante est attendue.
#include <stdio.h>
constexpr int TAILLE = 4;constexpr double TVA = 0.2;
int notes[TAILLE];
int main(void){ printf("%zu cases, TVA %.0f %%\n", sizeof notes / sizeof notes[0], TVA * 100); return 0;}$ gcc -Wall -Wextra constexpr.c -o constexpr && ./constexpr4 cases, TVA 20 %Le compilateur vérifie aussi que la valeur tient exactement dans le type. Un unsigned char ne peut pas contenir 300 : avec const, GCC tronquerait à 44 avec un simple warning ; avec constexpr, c'est une erreur.
$ gcc -c constexpr-bad.cconstexpr-bad.c:1:33: warning: unsigned conversion from 'int' to 'unsigned char' changes value from '300' to '44' [-Woverflow] 1 | constexpr unsigned char OCTET = 300; | ^~~constexpr-bad.c:1:33: error: 'constexpr' initializer not representable in type of objectEn C23, constexpr ne s'applique qu'aux objets, pas aux fonctions comme en C++, et pas aux pointeurs autres que nuls. Pour une constante entière, il remplace avantageusement un #define : elle a un type, une portée, et le débogueur la connaît.
typeof et auto : laisser le compilateur trouver le type
typeof(expr) donne le type d'une expression. GCC le proposait depuis longtemps comme extension, C23 le normalise, avec typeof_unqual qui donne le même type sans ses qualificatifs (const, volatile...). L'usage classique est une macro qui marche pour n'importe quel type. auto, réutilisé avec un nouveau sens, déduit le type d'une variable de sa valeur initiale.
#include <stdio.h>
#define TYPE(x) _Generic((x), int: "int", double: "double", \ char *: "char *", default: "autre")#define ECHANGE(a, b) do { typeof(a) tmp_ = (a); (a) = (b); (b) = tmp_; } while (0)
int main(void){ auto n = 42; auto x = 3.5; auto s = "texte";
printf("n : %s, x : %s, s : %s\n", TYPE(n), TYPE(x), TYPE(s));
double a = 1.5, b = 2.5; ECHANGE(a, b); printf("a = %.1f, b = %.1f\n", a, b); return 0;}$ gcc -Wall -Wextra auto.c -o auto && ./auton : int, x : double, s : char *a = 2.5, b = 1.5auto exige une initialisation et un seul nom par déclaration, et le tableau "texte" devient un pointeur char *. Il ne s'applique pas aux types de retour ni aux paramètres des fonctions. Garde-le pour les types longs à écrire : auto n = 42; n'apprend rien au lecteur que int n = 42; ne disait déjà.
Les attributs : [[nodiscard]], [[maybe_unused]], [[deprecated]]
C23 adopte la syntaxe d'attributs [[...]] du C++. Trois servent tous les jours : [[nodiscard]] demande un warning si on ignore la valeur de retour, [[deprecated]] signale une fonction à ne plus utiliser, [[maybe_unused]] fait taire le warning d'un paramètre ou d'une variable volontairement inutilisés.
#include <stdlib.h>
[[nodiscard]] int *creer_tableau(size_t n){ return malloc(n * sizeof(int));}
[[deprecated("utilise creer_tableau")]] int *vieux_tableau(size_t n){ return calloc(n, sizeof(int));}
int main(int argc, [[maybe_unused]] char **argv){ int inutilise = argc;
creer_tableau(10); free(vieux_tableau(10)); return 0;}$ gcc -Wall -Wextra attributs.c -o attributsattributs.c: In function 'main':attributs.c:17:5: warning: ignoring return value of 'creer_tableau', declared with attribute 'nodiscard' [-Wunused-result] 17 | creer_tableau(10); | ^~~~~~~~~~~~~~~~~attributs.c:3:20: note: declared here 3 | [[nodiscard]] int *creer_tableau(size_t n) | ^~~~~~~~~~~~~attributs.c:18:5: warning: 'vieux_tableau' is deprecated: utilise creer_tableau [-Wdeprecated-declarations] 18 | free(vieux_tableau(10)); | ^~~~attributs.c:8:46: note: declared here 8 | [[deprecated("utilise creer_tableau")]] int *vieux_tableau(size_t n) | ^~~~~~~~~~~~~attributs.c:15:9: warning: unused variable 'inutilise' [-Wunused-variable] 15 | int inutilise = argc; | ^~~~~~~~~Le [[nodiscard]] a attrapé une vraie fuite : le tableau alloué ligne 17 n'est jamais libéré, puisque son adresse est jetée. argv n'est pas signalé grâce à [[maybe_unused]], alors que inutilise, sans attribut, l'est.
Littéraux binaires, séparateur ' et %b
Les entiers peuvent s'écrire en binaire avec le préfixe 0b, une extension de GCC que C23 rend standard. L'apostrophe sépare les chiffres pour la lisibilité, dans n'importe quelle base. Côté bibliothèque, printf gagne le format %b, qui affiche en binaire, et %#b qui ajoute le préfixe.
#include <stdio.h>
int main(void){ unsigned masque = 0b1010'0101; long million = 1'000'000;
printf("%u %b %#b\n", masque, masque, masque); printf("%ld\n", million); return 0;}$ gcc -Wall -Wextra binaire.c -o binaire && ./binaire165 10100101 0b101001011000000%b dépend de ta bibliothèque C et pas du compilateur : glibc le gère depuis sa version 2.35. Les détails des autres formats sont dans la fiche printf(3).
#embed : mettre un fichier dans un tableau
#embed lit un fichier au moment de la compilation et le remplace par la liste de ses octets, séparés par des virgules. Plus besoin d'outil externe pour convertir une image ou un texte en tableau C. GCC le gère depuis la version 15.
#include <stdio.h>
static const char message[] = {#embed "message.txt" , '\0'};
int main(void){ printf("%zu octets : %s", sizeof message - 1, message); return 0;}$ gcc -Wall -Wextra embed.c -o embed && ./embed12 octets : Bonjour C23message.txt contient « Bonjour C23 » suivi d'un retour à la ligne, soit 12 octets. Le fichier n'est pas une chaîne : il faut ajouter soi-même le '\0' final, d'où la virgule après la directive. La directive accepte des paramètres, comme limit(n) pour ne lire que les n premiers octets.
ckd_add : détecter un débordement sans comportement indéfini
Un int signé qui déborde est un comportement indéfini : écrire a + b puis vérifier si le résultat « a l'air faux » ne marche pas, le compilateur a le droit de supprimer ta vérification. <stdckdint.h> fournit ckd_add, ckd_sub et ckd_mul, qui calculent le résultat comme avec des entiers sans limite, le rangent dans la variable dont tu donnes l'adresse, et renvoient true s'il ne tient pas.
#include <limits.h>#include <stdckdint.h>#include <stdio.h>
int main(void){ int somme;
if (ckd_add(&somme, INT_MAX - 1, 1)) printf("débordement\n"); else printf("ok : %d\n", somme);
if (ckd_add(&somme, INT_MAX, 1)) printf("débordement, somme vaut %d\n", somme);
size_t octets; if (ckd_mul(&octets, (size_t)1 << 62, sizeof(int))) printf("taille trop grande pour un size_t\n"); return 0;}$ gcc -Wall -Wextra ckd.c -o ckd && ./ckdok : 2147483647débordement, somme vaut -2147483648taille trop grande pour un size_tEn cas de débordement, la variable reçoit le résultat ramené à sa largeur (ici INT_MIN), et c'est la valeur de retour qui compte. Le ckd_mul sur un size_t est le test à faire avant un malloc(n * taille) quand n vient de l'extérieur. Pour savoir écrire ces vérifications toi-même, sans l'en-tête, pratique sur checked_size pour la multiplication, ou ici pour l'addition :
add_overflows
Dis si a + b sortirait de l'intervalle d'un int, sans jamais calculer a + b. La méthode est celle que ckd_add t'épargne : comparer a à INT_MAX - b ou à INT_MIN - b selon le signe de b, au bit près sur les bornes.
<stdbit.h> : compter et manipuler les bits
<stdbit.h> réunit des opérations qu'on réécrivait à la main ou qu'on prenait dans les fonctions propres à GCC : compter les bits à 1, les zéros en tête ou en queue, la largeur utile, la puissance de 2 suivante. Chaque fonction existe pour chaque type non signé, plus une version générique.
#include <stdbit.h>#include <stdio.h>
int main(void){ unsigned x = 0b1011'0000;
printf("%u bits à 1\n", stdc_count_ones(x)); printf("%u zéros à droite\n", stdc_trailing_zeros(x)); printf("%u bits utiles\n", stdc_bit_width(x)); printf("%u, puissance de 2 suivante\n", stdc_bit_ceil(x)); printf("64 puissance de 2 : %d\n", stdc_has_single_bit(64u)); return 0;}$ gcc -Wall -Wextra stdbit.c -o stdbit && ./stdbit3 bits à 14 zéros à droite8 bits utiles256, puissance de 2 suivante64 puissance de 2 : 1Cet en-tête vient de la bibliothèque C : glibc le fournit depuis sa version 2.39.
static_assert sans message et initialiseur {}
static_assert devient un mot-clé (plus besoin de <assert.h>) et le message devient facultatif. Les accolades vides {} initialisent tout à zéro, y compris un tableau de taille variable, ce que {0} ne permettait pas.
#include <stdio.h>
struct point { int x, y; };
static_assert(sizeof(int) >= 4);
int main(void){ int n = 3; int t[n] = {}; struct point p = {};
printf("%d %d %d / %d %d\n", t[0], t[1], t[2], p.x, p.y); return 0;}$ gcc -Wall -Wextra init.c -o init && ./init0 0 0 / 0 0$ gcc -c assert-bad.cassert-bad.c:1:1: error: static assertion failed 1 | static_assert(sizeof(long) == 4); | ^~~~~~~~~~~~~assert-bad.c contient la seule ligne affichée : sur cette machine 64 bits, un long fait 8 octets, et la compilation s'arrête.
strdup et strndup entrent dans la norme
strdup(3) existait depuis des décennies sous POSIX, mais pas dans la norme C : en mode strict, <string.h> ne la déclarait pas. C23 l'ajoute, avec strndup.
$ gcc -std=c17 -Wall -Wextra strdup.c -o strdupstrdup.c: In function 'main':strdup.c:7:19: error: implicit declaration of function 'strdup'; did you mean 'strcmp'? [-Wimplicit-function-declaration] 7 | char *copie = strdup("Grademe"); | ^~~~~~ | strcmpstrdup.c:7:19: error: initialization of 'char *' from 'int' makes pointer from integer without a cast [-Wint-conversion]$ gcc -std=c23 -Wall -Wextra strdup.c -o strdup && ./strdupGrademeLe même fichier, une strdup suivie d'un free, échoue en C17 strict et compile en C23. En mode gnu17, l'ancien défaut, glibc la déclarait déjà, ce qui explique qu'on l'ait toujours utilisée sans y penser.
unreachable() : promettre qu'un chemin est impossible
unreachable(), dans <stddef.h>, dit au compilateur qu'une ligne ne sera jamais atteinte, ce qui l'autorise à optimiser en conséquence. Si elle l'est quand même, c'est un comportement indéfini. En développement, -fsanitize=undefined transforme cette promesse en vérification :
#include <stddef.h>#include <stdio.h>
enum couleur { ROUGE, VERT };
const char *nom(enum couleur c){ switch (c) { case ROUGE: return "rouge"; case VERT: return "vert"; } unreachable();}
int main(void){ printf("%s\n", nom(VERT)); printf("%s\n", nom(2)); return 0;}$ gcc -Wall -Wextra -g -fsanitize=undefined unreachable.c -o unreachable && ./unreachablevertunreachable.c:12:5: runtime error: execution reached an unreachable program pointnom(2) passe une valeur qui ne correspond à aucune couleur, et l'exécution atteint la ligne 12. Sans le sanitizer, en -O2, personne ne sait ce que fera le programme. Réserve unreachable() aux cas que ton code garantit vraiment, sinon tu écris toi-même le bug que le comportement indéfini rendra invisible.
Ce qui dépend de ta bibliothèque C
Le compilateur ne fait pas tout. %b, <stdbit.h>, strdup en mode strict ou memset_explicit (un memset que l'optimiseur n'a pas le droit de supprimer, pour effacer un mot de passe) sont fournis par la bibliothèque C. Avec glibc 2.41, celle de Debian 13, memset_explicit n'existe pas encore :
$ gcc -Wall -Wextra memset.c -o memsetmemset.c: In function 'main':memset.c:7:5: error: implicit declaration of function 'memset_explicit' [-Wimplicit-function-declaration] 7 | memset_explicit(mdp, 0, sizeof mdp); | ^~~~~~~~~~~~~~~$ ldd --version | head -n 1ldd (Debian GLIBC 2.41-12+deb13u4) 2.41Elle est arrivée dans glibc 2.43. Sur ta machine, ldd --version donne la version installée.
Tableau récapitulatif
| Nouveauté | En-tête | Géré depuis |
|---|---|---|
nullptr, nullptr_t | <stddef.h> pour nullptr_t | GCC 13 |
bool, true, false mots-clés | aucun | GCC 13 |
constexpr pour les objets | aucun | GCC 13 |
typeof, typeof_unqual | aucun | GCC 13 (typeof avant, en extension) |
auto pour les variables | aucun | GCC 13 |
[[deprecated]] | aucun | GCC 10 |
[[nodiscard]] | aucun | GCC 11 |
[[maybe_unused]] | aucun | GCC 10, GCC 13 sur les étiquettes |
littéraux 0b | aucun | GCC 4.3, en extension avant C23 |
séparateur ' | aucun | GCC 12 |
%b dans printf | <stdio.h> | glibc 2.35 |
#embed | aucun | GCC 15 |
ckd_add, ckd_sub, ckd_mul | <stdckdint.h> | GCC 14 |
| fonctions de bits | <stdbit.h> | glibc 2.39 |
static_assert sans message | aucun | GCC 9 |
initialiseur {} | aucun | GCC 13 |
strdup, strndup | <string.h> | glibc, déclarées en C23 strict |
unreachable() | <stddef.h> | GCC 13 |
memset_explicit | <string.h> | glibc 2.43 |
Avec GCC 13 ou 14, celles que le compilateur gère demandent -std=c2x (GCC 14 accepte aussi -std=c23). Avec GCC 15, elles sont actives par défaut.
Questions fréquentes
Faut-il ajouter -std=c23 pour utiliser C23 ?
Pas avec GCC 15, qui compile en gnu23 par défaut. Avec GCC 14, utilise -std=c23 ; avec GCC 13, -std=c2x. -std=c23 désactive seulement les extensions GNU qui contredisent la norme ; pour être averti de toutes les autres, ajoute -Wpedantic.
Faut-il remplacer NULL par nullptr partout ?
Dans du code neuf compilé en C23, nullptr est plus sûr : son type est le même partout, alors que celui de NULL dépend de l'implémentation. Dans un projet qui doit encore compiler en C17, garde NULL.
Quelle différence entre const et constexpr ?
const interdit de modifier une variable, mais sa valeur peut n'être connue qu'à l'exécution, et elle n'est pas une constante au sens du compilateur. constexpr exige une valeur calculable à la compilation, vérifie qu'elle tient dans le type, et permet de s'en servir comme taille de tableau global ou dans un case.
auto en C fonctionne-t-il comme en C++ ?
Seulement pour déclarer une variable initialisée. C23 ne l'autorise ni pour le type de retour d'une fonction ni pour ses paramètres. Et auto garde aussi son ancien sens de classe de stockage, celui qu'on n'écrivait jamais.
Mon vieux code compile-t-il encore en C23 ?
Presque toujours, mais pas toujours : typedef int bool; et les déclarations f() sans void ne passent plus, et les définitions de fonctions à la K&R ne sont plus du C23 valide. Les erreurs et leurs corrections sont détaillées dans GCC 15 compile en C23 par défaut.
Sources
