Accès rapide :
La valeur de __STDC_VERSION__
bool, true et false
Des constantes typées avec constexpr
Le mot clé nullptr
auto, typeof et typeof_unqual
Les entiers à largeur précise
Littéraux binaires et séparateurs de chiffres
Enumérations à type fixé
char8_t et les chaînes UTF-8
static_assert
Attributs standards
Un préprocesseur plus pratique
Nouveaux en-têtes et fonctions utiles
Quelques nettoyages du langage
C23 constitue une évolution nettement plus visible que C17. Le langage reste du C, mais plusieurs éléments longtemps présents sous forme d'extensions de compilateurs, ou déjà familiers aux développeurs C++, entrent maintenant dans le standard. Comme toujours, il faut distinguer le support du compilateur et celui de la librairie C : vous pouvez parfois utiliser une syntaxe C23 alors que certains nouveaux en-têtes ne sont pas encore disponibles sur votre plateforme.
En mode C23, la macro __STDC_VERSION__ vaut 202311L. Avec un compilateur récent, vous pouvez demander
ce niveau de standard avec l'option -std=c23. Sur des versions plus anciennes de GCC ou Clang, vous rencontrerez
parfois encore l'option transitoire -std=c2x.
1 2 3 4 5 6 7 |
#include <stdio.h> int main() { printf( "__STDC_VERSION__ == %ld\n", __STDC_VERSION__ ); return 0; } |
Depuis C99, on utilisait l'entête <stdbool.h> pour obtenir les noms bool, true et
false. Avec C23, ces noms font directement partie du langage. Le code gagne donc en lisibilité, surtout pour les
débutants qui arrivent d'autres langages.
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <stdio.h> int main() { bool connected = true; if ( connected ) { printf( "Connexion active.\n" ); } return 0; } |
Le mot clé constexpr permet de définir de vraies constantes nommées, typées et utilisables dans des expressions
constantes. Il ne faut pas le confondre avec le constexpr du C++ : en C23, il concerne les objets, pas les fonctions.
C'est néanmoins très pratique pour remplacer certains #define ou certaines énumérations utilisées uniquement pour
produire une constante entière.
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <stdio.h> constexpr int BufferSize = 256; static_assert( BufferSize == 256 ); int main() { char buffer[BufferSize]; printf( "Taille du buffer : %zu octets\n", sizeof buffer ); return 0; } |
C23 introduit nullptr, une constante de pointeur nul plus explicite que NULL. L'objectif est d'éviter
certaines ambiguïtés liées au fait que NULL est historiquement défini par une macro. Le type associé,
nullptr_t, est notamment utile avec _Generic ou pour écrire des interfaces plus explicites.
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <stdio.h> int main() { int * pointer = nullptr; if ( pointer == nullptr ) { printf( "Le pointeur est nul.\n" ); } return 0; } |
C23 récupère aussi une idée longtemps disponible dans certains compilateurs : laisser le compilateur déduire un type local ou
récupérer le type d'une expression. Le mot clé auto sert maintenant à l'inférence de type pour des définitions
d'objets. Les opérateurs typeof et typeof_unqual permettent, eux, de réutiliser le type d'une expression.
Le second retire les qualificateurs comme const ou volatile.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <stdio.h> int main() { auto counter = 42u; /* unsigned int */ typeof( counter ) next = counter + 1u; const int limit = 10; typeof_unqual( limit ) value = limit; value++; printf( "next == %u\n", next ); printf( "value == %d\n", value ); return 0; } |
Le nouveau type _BitInt(N) permet de demander un entier représenté sur un nombre précis de bits. Cette fonctionnalité
vise surtout les traitements bas niveau, les formats binaires, les protocoles ou certains calculs embarqués. Pour du code
applicatif classique, les types de <stdint.h> restent souvent plus simples à lire.
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <stdio.h> int main() { unsigned _BitInt(5) flags = 0b1'0110; _BitInt(12) temperature = -125; printf( "flags == %u\n", (unsigned) flags ); printf( "temperature == %d\n", (int) temperature ); return 0; } |
C23 ajoute aussi des écritures plus confortables pour certaines constantes numériques. Les littéraux binaires utilisent le
préfixe 0b ou 0B, et le caractère ' peut être utilisé comme séparateur visuel dans les
nombres. Le standard ajoute aussi des suffixes liés aux entiers à largeur précise, comme wb et uwb.
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <stdio.h> int main() { int mask = 0b1111'0000; int value = 1'000'000; printf( "mask == %d\n", mask ); printf( "value == %d\n", value ); return 0; } |
La famille printf gagne aussi un format binaire avec %b et %B. C'est agréable pour afficher
directement des masques de bits, mais il faudra vérifier le support réel de votre librairie C.
1 2 3 4 5 6 7 8 9 10 11 |
#include <stdio.h> int main() { unsigned int flags = 0b1010'1100u; printf( "flags == %b\n", flags ); printf( "flags == 0B%B\n", flags ); return 0; } |
Les énumérations peuvent maintenant préciser leur type sous-jacent. C'est utile quand la taille de stockage fait partie du contrat : format de fichier, protocole réseau, structure partagée avec du matériel, etc. Sans cette précision, le compilateur conserve une certaine liberté de représentation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <stdio.h> enum LogLevel : unsigned char { LOG_INFO = 1, LOG_WARNING = 2, LOG_ERROR = 3 }; int main() { enum LogLevel level = LOG_WARNING; printf( "Taille : %zu octet(s)\n", sizeof level ); printf( "Valeur : %u\n", (unsigned) level ); return 0; } |
C23 introduit le type char8_t, défini dans <uchar.h>, pour représenter explicitement des unités de
code UTF-8. Les chaînes préfixées par u8 ont donc un type plus précis qu'avant. C'est utile pour clarifier les
interfaces qui travaillent vraiment en UTF-8.
1 2 3 4 5 6 7 8 9 10 |
#include <stdio.h> #include <uchar.h> int main() { char8_t message[] = u8"KooR"; printf( "Taille : %zu octets\n", sizeof message ); return 0; } |
Les assertions statiques existaient déjà avec C11 sous la forme _Static_assert. C23 standardise l'écriture
static_assert, plus naturelle et plus proche de ce que l'on trouve en C++. Le message devient optionnel, ce qui
permet d'écrire des vérifications très courtes.
1 2 3 4 5 6 7 8 |
#include <limits.h> static_assert( CHAR_BIT == 8, "Cette plateforme n'utilise pas des octets de 8 bits" ); static_assert( sizeof( int ) >= 4 ); int main() { return 0; } |
C23 renforce la syntaxe des attributs avec la forme [[...]]. Certains attributs servent surtout à documenter une
intention auprès du compilateur : ne pas ignorer une valeur de retour, signaler une fonction obsolète, indiquer une variable
volontairement inutilisée, ou rendre explicite une chute entre deux branches d'un switch.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <stdio.h> [[nodiscard]] int load_configuration() { return 0; } int main() { [[maybe_unused]] int debugCounter = 0; load_configuration(); /* Le compilateur peut signaler cet oubli. */ return 0; } |
Le préprocesseur reçoit plusieurs ajouts utiles. Les directives #elifdef et #elifndef évitent des
expressions conditionnelles inutilement longues. La macro __VA_OPT__ simplifie les macros variadiques dont la partie
variable peut être vide. On trouve aussi des tests comme __has_include ou __has_c_attribute, ainsi que
#embed pour incorporer des données externes dans une unité de traduction.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <stdio.h> #if __has_include(<threads.h>) #include <threads.h> #elifdef USE_PTHREADS #include <pthread.h> #else #warning "Aucune librairie de threads détectée" #endif #define LOG(format, ...) fprintf( stderr, format __VA_OPT__(,) __VA_ARGS__ ) int main() { LOG( "Démarrage\n" ); LOG( "Valeur : %d\n", 42 ); return 0; } |
1 2 3 |
static const unsigned char icon[] = { #embed "icon.bin" }; |
#embed est un ajout très pratique, mais son support est encore récent. Vérifiez votre chaîne de compilation avant
de l'utiliser dans un projet portable.
C23 n'ajoute pas uniquement de la syntaxe. La librairie standard évolue elle aussi, avec notamment
<stdckdint.h> pour les opérations arithmétiques vérifiées, <stdbit.h> pour certaines opérations
sur les bits, memset_explicit pour effacer explicitement des données sensibles, ainsi que des fonctions déjà connues
dans certains environnements POSIX comme strdup, strndup ou memccpy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <stdio.h> #include <stdckdint.h> int main() { int result; if ( ckd_add( &result, 2'000'000'000, 2'000'000'000 ) ) { printf( "Débordement détecté.\n" ); return 1; } printf( "result == %d\n", result ); return 0; } |
1 2 3 4 5 6 7 8 9 10 |
#include <stdio.h> #include <stdbit.h> int main() { unsigned int flags = 0b1011u; printf( "Nombre de bits à 1 : %u\n", stdc_count_ones( flags ) ); return 0; } |
C23 retire ou clarifie aussi plusieurs formes historiques du langage. Les définitions de fonctions à l'ancienne, héritées du C
K&R, disparaissent du standard. Les déclarations implicites de fonctions et le vieux int implicite ne doivent plus
être considérés comme du C moderne. Autre point important : une liste de paramètres vide indique maintenant réellement qu'une
fonction ne prend pas de paramètre, ce qui rapproche l'écriture int main() de int main( void ).
1 2 3 4 5 6 7 8 |
/* A éviter dans du code C moderne. */ int legacy(); /* Plus explicite. */ int compute(void); int compute(void) { return 42; } |
Au final, C23 est une mise à jour importante, mais il ne faut pas se précipiter sans vérifier l'outillage. Beaucoup d'ajouts sont déjà très utiles pour écrire du C plus clair, mais leur disponibilité dépendra encore pendant quelque temps du compilateur, de la librairie C et des options de compilation utilisées.
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :