URL: https://linuxfr.org/news/nouveautes-du-langage-c-dans-sa-prochaine-version-c23 Title: NouveautĂ©s du langage C dans sa prochaine version C23 Authors: pulkomandy bayo, vmagnin, Yves Bourguignon, Ysabeau đŸ§¶, Lawless, xdelatour, Julien Jorge, nico4nicolas, gUI, Cm, Gil Cot ✔, Christophe G. et alkino Date: 2022ćčŽ08月07æ—„T23:22:02+02:00 License: CC By-SA Tags: langage_c et c Score: 121 Le C est un langage de programmation dĂ©veloppĂ© depuis 1972 par Kenneth Thompson, Brian Kernighan et Dennis Ritchie. Il est, au dĂ©part, Ă©troitement liĂ© au dĂ©veloppement du systĂšme UNIX, mais il a par la suite trouvĂ© de nombreuses autres utilisations. Il a influencĂ© le dĂ©veloppement de plusieurs autres langages dont C++, Objective-C, Java, D et C#. La version C23, qui sera vraisemblablement finalisĂ©e en 2023, apporte son lot de nouveautĂ©s. AprĂšs un bref historique de la normalisation du langage, cet article parcourt les principaux changements prĂ©sents dans cette nouvelle version. ---- [C23 - cppreference.com](https://en.cppreference.com/w/c/23) [C23 is shaping up: New keywords and fixed assert to land in updated standard](https://devclass.com/2022/02/22/c23-update-adds-fixes-and-keywords/) [C2x - wikipedia](https://en.wikipedia.org/wiki/C2x) [C23 is Finished: Here is What is on the Menu - The Pasture](https://thephd.dev/c23-is-coming-here-is-what-is-on-the-menu) [C23 implications for C libraries](https://gustedt.gitlabpages.inria.fr/c23-library/) [Le "brouillon" de la spĂ©cification C23 du 3 septembre 2022](https://open-std.org/JTC1/SC22/WG14/www/docs/n3054.pdf) ---- Normalisation =============== La premiĂšre version stable du langage est celle publiĂ©e en 1978 dans le livre *The C Programming Language*. Les premiers compilateurs ont Ă©tĂ© implĂ©mentĂ©s en suivant ce livre, cependant il ne s’agit pas d’une spĂ©cification du langage et le comportement des compilateurs n’était pas toujours identique sur des cas inhabituels. C’est pourquoi un groupe de travail de l’ANSI s’est chargĂ©, Ă  partir de 1983, de rĂ©diger une spĂ©cification plus formelle, qui clarifie toutes les zones d’ombre laissĂ©es par le livre de 1978. Cela a pris beaucoup de temps et cette spĂ©cification ne sera publiĂ©e qu’en 1989. Il faut noter que dĂšs cette version, le C s’inspire du C++ qui est encore un tout jeune langage, et intĂšgre par exemple la notion de prototype de fonction qui n’était pas prĂ©sente dans les versions prĂ©cĂ©dentes. La version normalisĂ©e du C passe ensuite dans les mains de l’ISO, qui republiera d’abord la norme ANSI, puis deux volumes de « corrections techniques » en 1994 et 1996 pour corriger certaines erreurs mineures dans la spĂ©cification et ajouter quelques petits changements. Mais la premiĂšre grosse mise Ă  jour du C normalisĂ© arrive en 1999. On trouve en C99 les tableaux de taille variable, les pointeurs restreints, les nombres complexes, les littĂ©raux composĂ©s, les dĂ©clarations mĂ©langĂ©es avec les instructions, les fonctions `inline`, le support avancĂ© des nombres flottants, et une nouvelle syntaxe de commentaire inspirĂ©e de C++. La version suivante, encore plus de dix ans plus tard, est C11, qui ajoute les threads, les expressions Ă  type gĂ©nĂ©rique, et une meilleure prise en charge de l’Unicode. Elle rend Ă©galement optionnelles certaines fonctionnalitĂ©s qui Ă©taient obligatoires en C++. Par la suite elle est mise Ă  jour par la version C17, qui ne comporte que des clarifications et corrections sans apporter de grandes nouveautĂ©s. Comme toutes les normes publiĂ©es par l’ISO, la version finalisĂ©e des documents n’est accessible que moyennant paiement. Cependant, le comitĂ© de normalisation met Ă  disposition des versions « brouillon » proches de la version finale, qui permettent de se faire une idĂ©e trĂšs prĂ©cise du contenu de la norme. On peut Ă©galement consulter les « [notes](https://www.open-std.org/jtc1/sc22/wg14/www/wg14_document_log) », toutes sortes de documents liĂ©s au processus de normalisation. Certaines de ces notes contiennent des propositions de changement de la norme, cependant, elles sont soumises Ă  un vote et peuvent ĂȘtre rejetĂ©es. Une fois qu’un document est rejetĂ©, il est mal vu de le proposer Ă  nouveau sans d’importantes modifications. Aussi, le travail pour proposer des modifications dans la norme se compose au moins autant de politique et de nĂ©gociations que de travail pour rĂ©diger la proposition technique. La proposition de changements est ouverte Ă  tout le monde selon des [rĂšgles bien documentĂ©es](https://www.open-std.org/jtc1/sc22/wg14/www/contributing.html). Les propositions sont ensuite revues lors des rĂ©unions du comitĂ© de normalisation qui choisit, ou pas, de les intĂ©grer dans la norme. Cependant, il est recommandĂ© de prendre contact avec les membres du comitĂ© pour arriver Ă  rĂ©diger une proposition qui aura une chance de passer cet examen. En plus des activitĂ©s d’intĂ©gration de nouvelles fonctionnalitĂ©s, le comitĂ© doit aussi rĂ©gler des problĂšmes administratifs et techniques. Par exemple, la spĂ©cification du C Ă©tait Ă©crite Ă  l’aide de troff et avait des problĂšmes avec les outils modernes pour manipuler ce langage. Il y a donc eu une migration vers LaTeX. De mĂȘme, en 2020 le groupe de travail a dĂ» se rĂ©organiser pour faire des meetings virtuels, ce qui n’était pas le cas auparavant. Les trucs supprimĂ©s =========== DĂ©finition de fonctions de style « K&R » ---------------------------------------- [N2432](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2432.pdf) Cette façon de dĂ©finir des fonctions date d’avant la premiĂšre version normalisĂ©e du C (C89). Elle est obsolĂšte depuis longtemps, Ă  tel point qu’elle Ă©tait dĂ©jĂ  dĂ©clarĂ©e obsolĂšte dans la version C89. Elle ressemble Ă  ça : ```C int add(a, b) int a; int b; { return a + b; } ``` Cette forme est maintenant interdite et il faudra utiliser la « nouvelle » syntaxe qui indique les types des paramĂštres dans les parenthĂšses : ```C int add(int a, int b) { return a + b; } ``` DĂ©claration de fonctions sans spĂ©cifier les paramĂštres ------------------------------------------------------ [N2841](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2841.htm) Le code suivant compilait sans problĂšme dans les versions prĂ©cĂ©dentes du C, mais en C23 ce ne sera plus le cas : ```C extern int foo(); void bar(void) { int a = 0, b = 1, c = 2; foo(a, b, c); } ``` Le comportement de la dĂ©claration de foo ci-dessus sera Ă©quivalent Ă  : ```C extern int foo(void); ``` C’est le mĂȘme comportement qu’en C++. La dĂ©claration de fonctions sans spĂ©cifier les paramĂštres Ă©tait dĂ©conseillĂ©e depuis la version C89, il Ă©tait donc temps de la retirer du langage. Fonctions sans arguments fixes ------------------------------ [N2975](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2975.pdf) On peut se demander pourquoi la dĂ©claration sans paramĂštres n’a pas Ă©tĂ© supprimĂ©e plus tĂŽt. La raison est qu’il existe un cas ou cette syntaxe Ă©tait utile. En effet, c’était la seule façon de dĂ©clarer une fonction pouvant accepter un nombre quelconque de paramĂštres, y compris aucun. Pour arriver au mĂȘme rĂ©sultat, une autre fonctionnalitĂ© du langage a Ă©tĂ© revue, il s’agit des fonctions variadiques. Habituellement une fonction variadique s’implĂ©mente de cette façon : ```C int printf(int format, ...) { va_list ap; va_start(ap, format); // on dĂ©clare le dernier argument "connu" if (format == INTEGER) { int valeur = va_arg(ap, int); } if (format == DOUBLE) { double valeur = va_arg(ap, double); } va_end(ap); } ``` On remarque que la macro `va_start` prend en paramĂštre le nom du dernier paramĂštre connu. Ce qui empĂȘche d’écrire une fonction de ce type : ```C int foo(...) { } ``` La macro `va_start` est modifiĂ©e en C23 pour ignorer ce deuxiĂšme argument. On pourra donc dĂ©sormais Ă©crire : ```C int printf(int format, ...) { va_list ap; va_start(ap); if (format == INTEGER) { int valeur = va_arg(ap, int); } if (format == DOUBLE) { double valeur = va_arg(ap, double); } va_end(ap); } ``` Le code existant continuera toutefois de compiler sans erreur, la macro pouvant toujours recevoir un deuxiĂšme argument dont elle ne fera rien. Et on pourra utiliser cela pour dĂ©clarer des fonctions avec n’importe quel nombre d’arguments, en remplacement de l’ancienne notation. Il s’agit en quelque sorte d’un retour en arriĂšre, puisque la fonction `va_start()` fonctionnait de cette façon avant sa normalisation dans `stdarg.h` (on la trouvait alors dans `vararg.h` qui est toujours disponible dans GCC par exemple). Le deuxiĂšme argument avait Ă©tĂ© ajoutĂ© pour satisfaire aux besoins de certains compilateurs et exposait un dĂ©tail d’implĂ©mentation et d’ABI qui n’a finalement rien Ă  faire dans la spĂ©cification du langage. Suppression des trigraphes ??! ------------------------------ Le langage C utilise peu de caractĂšres spĂ©ciaux afin de pouvoir fonctionner avec n’importe quel code de caractĂšres sur 7 bits. Le jeu de caractĂšres de base est [ISO 646](https://fr.wikipedia.org/wiki/ISO/CEI_646) qui ne comporte que 82 caractĂšres invariants (plus 16 caractĂšres de contrĂŽle non imprimables, et 12 caractĂšres variables qui sont remplacĂ©s dans la variante de cet encodage adaptĂ©e Ă  chaque langue). Ces 82 caractĂšres sont insuffisants, aussi le C permet de substituer des sĂ©quences de trois caractĂšres aux caractĂšres non disponibles (le remplacement est fait par le prĂ©processeur avant tout autre traitement). Huit caractĂšres supplĂ©mentaires hors ISO 646 sont ainsi pris en charge. ```c // Du code sans trigraphes #include int main(void) { int tab[3]; return 0; } ``` ```c // Le mĂȘme avec des trigraphes ??=include int main(void) ??< int tab??(3??); return 0; ??> ``` Cependant, mĂȘme sur les machines utilisant un encodage ISO 646, cette solution n’était pas vraiment populaire et il Ă©tait en gĂ©nĂ©ral possible d’utiliser d’autres caractĂšres de substitution, le code ressemblant par exemple Ă  ceci : ```c // Le mĂȘme en utilisant l’encodage ISO-646-FR ÂŁinclude int main(void) Ă© int tab°3§; return 0; Ăš ``` Aujourd’hui plus aucun systĂšme n’est limitĂ© Ă  un encodage du texte sur 7 bits. On utilise donc des encodages basĂ©s sur ASCII, ou, dans le pire des cas, EBCDIC, dans lesquels les caractĂšres nĂ©cessaires Ă  l’écriture du C sont tous disponibles. Les trigraphes sont donc supprimĂ©s en C23 (cela avait dĂ©jĂ  Ă©tĂ© fait pour C++ dans sa version C++17). Le fait d’avoir Ă©tendu le jeu de caractĂšres nĂ©cessaires a Ă©galement Ă©tĂ© l’occasion d’ajouter quelques autres caractĂšres obligatoires pour pouvoir stocker du code source : `@`, `$`, et `` ` `` ([N2701](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2701.htm)). Ils ne sont pas utilisĂ©s directement par le langage, mais peuvent ĂȘtre utiles par exemple dans les commentaires de type Doxygen : ```c /** @brief Une fonction avec un commentaire de type doxygen. * @param[in] a: un paramĂštre * @return: une valeur */ int uneFonction(int a); ``` Ou encore pour mettre des adresses e-mails dans des chaĂźnes de caractĂšres ou dans des commentaires. Ces 3 caractĂšres Ă©tant prĂ©sents dans ASCII et dans la plupart des variantes de EBCDIC, ils ont pu ĂȘtre ajoutĂ©s sans poser de contraintes supplĂ©mentaires. Les entiers signĂ©s sont forcĂ©ment en complĂ©ment Ă  deux ----------- Dans l’histoire de l’informatique, les nombres signĂ©s n’ont pas toujours Ă©tĂ© reprĂ©sentĂ©s de la mĂȘme façon. Certaines architectures de processeurs utilisaient par exemple un entier non signĂ© combinĂ© avec un bit de signe sĂ©parĂ©. Par exemple, si 1 est reprĂ©sentĂ© par `0 0000001`, -1 est reprĂ©sentĂ© par `1 0000001`. D’autres utilisent la nĂ©gation bit Ă  bit du nombre Ă  reprĂ©senter. On parle alors de [complĂ©ment Ă  un](https://fr.wikipedia.org/wiki/Compl%C3%A9ment_%C3%A0_un). Par exemple, si 1 est reprĂ©sentĂ© par `00000001`, alors -1 sera reprĂ©sentĂ© par `11111110`. Le langage C ne prenait pas position sur ce sujet et Ă©tait indĂ©pendant du format de reprĂ©sentation utilisĂ©. Ce qui permettait de faire fonctionner facilement un compilateur C sur n’importe quel processeur, mais avec une consĂ©quence importante : les opĂ©rations de conversion entre entiers signĂ©s et non signĂ©s provoquaient de nombreux cas de comportements indĂ©finis ou, au mieux, dĂ©finis par l’implĂ©mentation. Plus aucun processeur moderne n’utilise autre chose que le [complĂ©ment Ă  deux](https://fr.wikipedia.org/wiki/Compl%C3%A9ment_%C3%A0_deux). Cette façon de faire semble moins Ă©vidente que les deux autres, mais c’est en rĂ©alitĂ© la plus simple Ă  mettre en place car elle Ă©limine la plupart des cas particuliers. Les mĂȘmes instructions du processeur peuvent ĂȘtre utilisĂ©es pour les nombres positifs et nĂ©gatifs dans presque tous les cas. C’est donc maintenant le seul format autorisĂ© par le langage C, ce qui a permis de grandement simplifier la spĂ©cification du langage et en particulier le comportement des conversions entre types signĂ©s et non signĂ©s. Changements sur la gestion des caractĂšres Unicode ------ Les littĂ©raux prĂ©fixĂ©s par `u` ou `U` sont forcĂ©ment de l’UTF-16 et de l’UTF-32 ([N2728](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2728.htm)). Dans les versions prĂ©cĂ©dentes de C, le support de l’Unicode avait d’abord Ă©tĂ© ajoutĂ© avec le type `wchar_t` et les littĂ©raux utilisant le prĂ©fixe `L`. Cependant, aucun encodage spĂ©cifique n’était imposĂ©, et donc `wchar_t` pouvait ĂȘtre soit un type sur 16 bits, soit sur 32 bits, et pas forcĂ©ment en Unicode. Ensuite, les types `char16_t` et `char32_t` ont Ă©tĂ© ajoutĂ©s, avec les prĂ©fixes correspondants `u` et `U`. Mais il n’était toujours pas obligatoire d’utiliser de l’Unicode. En C23, c’est dĂ©sormais le cas, et tout autre encodage est interdit. ```c int main(void) { wchar_t* exemple1 = L"Une chaĂźne dans un encodage non spĂ©cifiĂ©"; char16_t* exemple2 = u"Une chaĂźne encodĂ©e en UTF-16"; char32_t* exemple3 = U"Une chaĂźne encodĂ©e en UTF-32"; } ``` De plus, il est interdit de mĂ©langer les diffĂ©rents prĂ©fixes : ```c int main(void) { // Code qui n’est plus valide en C23 : char32_t* exemple3 = U"Et si on mĂ©langeait" u" les encodages?"; } ``` Changement du comportement de realloc ------ Le comportement de `realloc` lorsqu’on donne 0 comme deuxiĂšme paramĂštre devient indĂ©fini. Auparavant il Ă©tait laissĂ© au choix de l’implĂ©mentation et pouvait par exemple, faire l’équivalent d’un `free`, ou ne rien faire et conserver le pointeur original. Et la fonction peut aussi, peut-ĂȘtre, mettre Ă  jour `errno`. La rĂ©daction de la spĂ©cification avait dĂ©jĂ  Ă©tĂ© modifiĂ©e en C17 suite Ă  des diffĂ©rences constatĂ©es entre les diffĂ©rentes implĂ©mentations dans diffĂ©rents systĂšmes lors de la mise Ă  jour de la spĂ©cification POSIX par l’Austin Group (sans que la spĂ©cification puisse dire qui avait raison). Le problĂšme a Ă©tĂ© remontĂ© via le [_defect report_ numĂ©ro 400](https://www.open-std.org/jtc1/sc22/wg14/www/docs/summary.htm#dr_400). Cependant, la correction qui a Ă©tĂ© faite et intĂ©grĂ©e dans C17 ne rĂ©solvait pas complĂštement le problĂšme, ce qui a Ă©tĂ© soulignĂ© via la [demande de clarification N2428](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2438.htm). Il y a deux cas Ă  tester : si le pointeur passĂ© en paramĂštre est valide, et s’il ne l’est pas. Et il y a trois choses Ă  vĂ©rifier en sortie : le pointeur retournĂ© par `realloc`, le fait que la mĂ©moire pointĂ©e par le pointeur passĂ© en paramĂštre a Ă©tĂ© libĂ©rĂ©e, et une Ă©ventuelle mise Ă  jour de `errno`. On trouvait dans diffĂ©rentes implĂ©mentations de C99 un peu tous les comportements possibles dans ces cas. Finalement, en C23 le comportement de realloc avec une taille de 0 devient indĂ©fini ([N2464](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2464.pdf)). Il faudra utiliser `free()` si on veut libĂ©rer de la mĂ©moire, et rien si on ne veut rien faire. Comme ça il n’y a plus d’ambiguĂŻtĂ©s dans la spĂ©cification du langage. De son cĂŽtĂ©, POSIX avait dĂ©jĂ  adoptĂ© une dĂ©finition plus claire du comportement, sur laquelle on pourra compter pour les systĂšmes compatibles POSIX mais pas forcĂ©ment ailleurs. Le comportement pour les systĂšmes POSIX est que l’ancien pointeur n’est pas libĂ©rĂ©, et que realloc retourne un pointeur valide vers une zone de 0 octet (qui peut ĂȘtre redimensionnĂ©e via de futurs appels Ă  realloc). ```C void* ptr = realloc(NULL, 0); // Utilisation valide de realloc avec une taille de 0, pour crĂ©er un pointeur vers une zone vide fd = open("data.bin", "r"); while(1) { // RĂ©cupĂ©ration d’une taille mĂ©moire Ă  utiliser uint32_t size = 0; int ret = read(fd, &size, sizeof(size)); if (ret == 0) { // fin du fichier break; } // Allocation de la mĂ©moire pour lire le bloc (fonctionne mĂȘme si la taille du bloc est de 0) void* ptr2 = realloc(ptr, size); if (ptr2 == NULL) { // problĂšme d’allocation, on quitte la boucle pour libĂ©rer les ressources break; } else { // rĂ©allocation rĂ©ussie, on peut oublier l’ancien pointeur et utiliser le nouveau ptr = ptr2; } // Lecture du bloc read(fd, ptr, size); } // realloc(ptr, 0); // Utilisation obsolĂšte de realloc pour libĂ©rer la mĂ©moire, qui ne fonctionne plus. free(ptr); // La bonne façon de faire close(fd); return -1; ``` stdbool.h --------- En C99, une premiĂšre tentative d’ajout de `bool`, `true` et `false` dans le langage avait Ă©tĂ© faite, pour assurer la compatibilitĂ© avec C++ qui en avait fait un type et deux mots clĂ©s du langage. Cependant, de nombreux programmes C avaient leur propre dĂ©finition du type bool (par exemple en utilisant un `typedef`) et des valeurs true et false (par un `#define` ou une Ă©numĂ©ration). En faire des mots clĂ©s rĂ©servĂ©s pour le langage C aurait cassĂ© ces programmes. La solution mise en place a donc Ă©tĂ© un peu plus compliquĂ©e : - Ajout du type _Bool dans le langage C (ce nom commençant par un _ suivi d’une lettre majuscule, par convention, les noms de ce type sont rĂ©servĂ©s pour l’usage interne du compilateur et donc le code C existant suivant la norme Ă  la lettre ne devrait pas y voir de problĂšme) - Ajout de l’en-tĂȘte stdbool.h qui peut ĂȘtre inclus pour pouvoir utiliser `bool`, `true` et `false` (sous forme de `#define`) Cette solution est finalement peu pratique, et ne rĂ©sout pas vraiment le problĂšme de compatibilitĂ© avec le C++. Finalement, en C23, `true` et `false` sont des _constantes prĂ©dĂ©finies_, un concept ajoutĂ© au langage C pour l’occasion (mais utilisĂ© ensuite aussi pour `nullptr`) grĂące Ă  la note [N2935](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2935.pdf). Tout le fichier stdbool.h est donc maintenant obsolĂšte. Retrait de `__alignof_is_defined`, `__alignas_is_defined` -------------------------------------------------------------- Le type `bool`, les macros `alignof` et `alignas`, et le qualificateur `thread_local` reçoivent un traitement similaire. Ils ont tous Ă©tĂ© introduits avec un mot clĂ© commençant par un `_` et une majuscule, et divers fichiers d’en-tĂȘte ajoutant des macros pour le nom usuel : ```C # define alignas _Alignas # define alignof _Alignof # define bool _Bool # define static_assert _Static_assert # define thread_local _Thread_local ``` Avec [N2934](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2934.pdf), ces dĂ©finitions sont dĂ©sormais disponibles sans avoir besoin d’inclure aucun fichier d’en-tĂȘte. Cela signifie Ă©galement qu’il n’y a plus besoin de `__alignof_is_defined`, `__alignas_is_defined`, on pourra tester plus directement la prĂ©sence de ces macros : ```c #ifndef alignof #error alignof n’est pas dĂ©fini #endif ``` Les premiĂšres versions de cette note tentaient de transformer ces noms en vĂ©ritables mots clĂ©s rĂ©servĂ©s du langage, mais cela causait encore trop de problĂšmes de compatibilitĂ© avec le code existant. Peut-ĂȘtre dans une prochaine version du C aprĂšs quelques dizaines d’annĂ©es supplĂ©mentaires ? Les fonctionnalitĂ©s qui deviennent obsolĂštes =========== ObsolĂštes (ou en jargon informatique *deprecated*) signifie que ces fonctionnalitĂ©s ne sont pas encore supprimĂ©es du langage. Mais ce sera probablement fait dans une prochaine version de la norme. Les macros pour dĂ©tecter le format des nombres flottants -------------------------------------------------------- Le langage C, essayant toujours de pouvoir s’implĂ©menter facilement sur n’importe quelle architecture matĂ©rielle et logicielle, n’impose pas de format d’encodage pour les nombres flottants (les types `float` et `double` et depuis C99, leurs Ă©quivalents pour les nombres complexes). Cependant, si une implĂ©mentation du langage C utilise le format le plus rĂ©pandu, elle peut l’indiquer via deux macros. Ainsi le code compilĂ© sur cette implĂ©mentation pourra dĂ©tecter que ce format est utilisĂ© et l’employer pour rĂ©aliser certaines optimisations. Le nom de ces macros Ă©tait `__STDC_IEC_559__` et `__STDC_IEC_559_COMPLEX__`. Cependant, l’IEC a [changĂ© sa numĂ©rotation en 1997](https://webstore.iec.ch/webstore/webstore.nsf/xpFAQ.xsp?OpenXPage&id=GFOT-7NXLKU) pour avoir des numĂ©ros compatibles avec ceux de l’ISO. La norme IEC 559 est donc devenue la norme ISO/IEC 60559. Le C23 introduit donc de nouvelles macros avec le nouveau nom, et en profite pour distinguer le support des nombres Ă  virgule flottante binaire (l’exposant est une puissance de 2) et des nombres Ă  virgule flottante dĂ©cimaux (l’exposant est une puissance de 10, ce qui Ă©vite des erreurs d’arrondi surprenantes pour les humains habituĂ©s Ă  rĂ©flĂ©chir en base 10) : - `__STDC_IEC_60559_BFP__` remplace `__STDC_IEC_559__` et indique que les nombres flottants binaires utilisent le format spĂ©cifiĂ© dans la norme ISO/IEC 60559, et que les fonctions mathĂ©matiques de la bibliothĂšque standard sont implĂ©mentĂ©es. - `__STDC_IEC_60559_DFP__` fait de mĂȘme pour les flottants dĂ©cimaux, - `__STDC_IEC_60559_COMPLEX__` remplace `__STDC_IEC_559_COMPLEX__` et fait de mĂȘme pour les nombres complexes. DĂ©tail amusant, bien que les macros utilisent le nom IEC 60559, la documentation parle de la norme IEEE 754, qui a le mĂȘme contenu mais est normalisĂ©e par l’ANSI et l’IEEE aux États-Unis. ```c #include #include #include #if __STDC_IEC_60559_COMPLEX__ int main(void) { double complex z1 = I * I; // i^2 printf("I * I = %.1f%+.1fi\n", creal(z1), cimag(z1)); double complex z2 = pow(I, 2); // i^2 aussi printf("pow(I, 2) = %.1f%+.1fi\n", creal(z2), cimag(z2)); double PI = acos(-1); double complex z3 = exp(I * PI); // La formule d'Euler: e^i*pi=-1 printf("exp(I*PI) = %.1f%+.1fi\n", creal(z3), cimag(z3)); } #else #error Les nombres complexes ne sont pas reprĂ©sentĂ©s dans le format spĂ©cifiĂ© par ISO/IEC 60599 #endif ``` DECIMAL_DIG ----------- [N2108](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2108.pdf) La constante `DECIMAL_DIG` a Ă©tĂ© ajoutĂ©e en C99 et donne le nombre de dĂ©cimales nĂ©cessaire pour reprĂ©senter un nombre de type `long double` sans perdre de prĂ©cision. Cependant, il existe dĂ©jĂ  une constante `LDBL_DECIMAL_DIG` avec la mĂȘme valeur (ainsi que des constantes Ă©quivalentes pour les autres types de nombres Ă  virgule flottante). DECIMAL_DIG est donc inutile, et devient obsolĂšte en C23. ```c long double unNombre; printf("%.*Lf", LDBL_DECIMAL_DIG, unNombre); ``` DĂ©finitions de macros redondantes dans math.h --------------------------------------------- Les dĂ©finitions de `INFINITY`, `DEC_INFINITY`, `NAN` et `DEC_NAN` Ă©taient disponibles Ă  la fois dans `` et ``. DĂ©sormais, seul ce dernier pourra ĂȘtre utilisĂ©. Les nouvelles fonctionnalitĂ©s ========= Le retour des ptrdiff_t sur 16 bits ----------------------------------- [N2808](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2808.htm) est un retour en arriĂšre sur un changement intervenu dans C99. Le type `ptrdiff_t` permet de calculer la diffĂ©rence entre deux pointeurs. Pour pouvoir traiter tous les cas, ce type doit forcĂ©ment ĂȘtre signĂ© (la diffĂ©rence entre deux pointeurs peut ĂȘtre nĂ©gative). Cependant, en C99, le type `ptrdiff_t` devait pouvoir reprĂ©senter des valeurs allant jusqu’à 65535, et il nĂ©cessitait donc au moins 17 bits. Cela n’est pas un problĂšme sur les systĂšmes utilisant des pointeurs plus gros, par exemple sur 32 bits. Mais c’est plus gĂȘnant pour les microcontrĂŽleurs (AVR, STM8...) sur lesquels il est souhaitable d’économiser la mĂ©moire autant que possible. Il est donc dommage de forcer ces architectures Ă  avoir un type `ptrdiff_t` plus large que le type des pointeurs. Ces implĂ©mentations sont donc autorisĂ©es Ă  utiliser un type 16 bits pour `ptrdiff_t`, et en consĂ©quence, une allocation mĂ©moire contiguĂ« ne devrait jamais dĂ©passer 32767 octets. ```c char data[40000]; ptrdiff_t size = &data[39999] - &data[0]; // peut-ĂȘtre interdit si vous ĂȘtes sur un systĂšme 16 bits ``` Distinction entre les tableaux Ă  taille variable et les types modifiĂ©s par des variables -------------------------------------------------------------------------------- [N2778](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2778.pdf) Le C99 a ajoutĂ© la possibilitĂ© de dĂ©clarer des tableaux dont la taille n’est pas connue Ă  la compilation : ```C void fonction(int n) { int tableau[n]; // allouĂ© sur la pile de façon similaire Ă  la fonction non-standard alloca() } ``` Ce type d’allocation n’est pas possible dans tous les cas, aussi, une implĂ©mentation du C peut choisir de ne pas autoriser ce code. Dans ce cas la macro `__STDC_NO_VLA__` doit ĂȘtre dĂ©finie. Une idĂ©e proche de celle des tableaux Ă  taille variable est les types modifiĂ©s par une variable, par exemple dans ce cas: ```C void foo(int n, double (*x)[n]) { (*x)[n] = 1; } ``` Il n’y a pas d’allocation dynamique dans ce cas (le tableau est juste passĂ© en paramĂštre), mais on indique au compilateur (via le [n] dans le paramĂštre de la fonction) quelle est la taille du tableau. Cela permet de dĂ©tecter les tentatives d’accĂšs au tableau Ă  des index hors de ses limites, Ă  la compilation ou Ă  l’exĂ©cution du code. Cette syntaxe Ă©tait auparavant interdite si la macro `__STDC_NO_VLA__` Ă©tait dĂ©finie. DĂ©sormais les deux concepts sont sĂ©parĂ©s et les types modifiĂ©s par des variables sont autorisĂ©s, mĂȘme si les tableaux Ă  taille variable ne le sont pas. En consĂ©quence, les types modifiĂ©s par une variable sont maintenant une fonctionnalitĂ© obligatoire dans les implĂ©mentations standards du C. nullptr (comme en C++) ---------------------- Un dĂ©faut historique du C est que la constante NULL peut ĂȘtre dĂ©finie soit comme un entier, soit comme un `void*`. De plus, c’est seulement une constante dĂ©finie par le prĂ©processeur (via un `#define`), ce qui empĂȘche de se reposer dessus dans les Ă©tapes suivantes de la compilation. Ces problĂšmes sont bien connus et ont dĂ©jĂ  Ă©tĂ© rĂ©solus dans C++11 par l’ajout de `nullptr` et du type `nullptr_t`. Ces changements ont Ă©tĂ© reportĂ©s dans la spĂ©cification du C par la note [N3042](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3042.htm). AmĂ©lioration des Ă©numĂ©rations ----------------------------- En C, les Ă©numĂ©rations sont implĂ©mentĂ©es avec un type sous-jacent. Par exemple on peut Ă©crire ceci: ```C enum { uneValeur = 4; }; int unEntier = uneValeur; ``` La norme ne spĂ©cifie pas un type fixe, et le compilateur peut choisir comme il veut. Par exemple, toutes les Ă©numĂ©rations peuvent ĂȘtre des entiers, ou bien un type est choisi en fonction des valeurs utilisĂ©es dans l’énumĂ©ration et du nombre de bits nĂ©cessaires pour les reprĂ©senter. Le problĂšme est que les constantes qui peuvent ĂȘtre indiquĂ©es dans une enum sont, d’aprĂšs la norme, forcĂ©ment de type `int`. Ceci est donc interdit: ```C enum a { a0 = 0xFFFFFFFFFFFFFFFFULL // il ne s’agit pas d’un int, mais d’un unsigned long long }; ``` En pratique, la plupart des compilateurs autorisent cette notation et il n’y a pas de problĂšme. D’ailleurs, c’est autorisĂ© dans les normes C++. La note [N3029](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3029.htm) propose donc d’autoriser cette notation pour dĂ©finir une Ă©numĂ©ration avec des valeurs en dehors de l’intervalle INT_MIN - INT_MAX. On peut penser que cet intervalle est largement suffisant, mais il ne faut pas oublier les implĂ©mentations du C oĂč le type `int` est un type sur seulement 16 bits, une limite qui peut ĂȘtre rapidement atteinte. Maintenant, ces implĂ©mentations du C pourront avoir des `int` sur 16 bits, mais des valeurs d’énumĂ©rations allant au-delĂ  si nĂ©cessaire. La note N3029 est assez longue, car elle prend beaucoup de prĂ©cautions pour Ă©viter des problĂšmes de compatibilitĂ©, y compris avec l’implĂ©mentation dĂ©jĂ  en place dans diffĂ©rents compilateurs C pour ce type de code. Ceci laisse quand mĂȘme un problĂšme assez gĂȘnant : c’est toujours le compilateur qui choisit le type d’une Ă©numĂ©ration. Cela rend difficile l’écriture de code portable sur de nombreux compilateurs et architectures. La note [N3030](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3030.htm) apporte donc une nouvelle syntaxe pour pouvoir forcer un type spĂ©cifique : ```C // Une Ă©numĂ©ration explicitant le type sous-jacent enum a : unsigned long long { a0 = 0xFFFFFFFFFFFFFFFFULL }; ``` Il s’agit, ici encore, d’une amĂ©lioration dĂ©jĂ  existante en C++ qui a Ă©tĂ© rĂ©cupĂ©rĂ©e dans la norme C. On peut Ă©galement obtenir une erreur Ă  la compilation si l’une des valeurs Ă©numĂ©rĂ©es ne peut pas ĂȘtre stockĂ©e dans le type choisi: ```C enum a : uint16_t { a0 = 0x10000 // erreur: il faudrait 17 bits pour stocker cette valeur et le type uint16_t n’en a que 16 }; ``` AmĂ©liorations des macros variadiques ------------------------------------ En C (normalisĂ© dans C99 mais cela existait dĂ©jĂ  dans la plupart des compilateurs), on peut dĂ©clarer des macros avec un nombre d’arguments variable : ```C // La macro LOG appelle fprintf avec stderr comme premier argument, suivi de tous les autres arguments sans modification #define LOG(X, ...) fprintf(stderr, X, __VA_ARGS__) ``` Cependant, ce systĂšme de macros est contre-intuitif, surtout lorsqu’on ne veut pas utiliser les arguments optionnels: ```C LOG("Valeur de x: %d\n", x); // remplacĂ© par fprintf(stderr, "Valeur de x: %d\n", x); // aucun problĂšme LOG("Bonjour\n"); // remplacĂ© par fprintf(stderr, "Bonjour\n",); // remarquez la virgule supplĂ©mentaire, ce code ne compile pas! ``` La plupart des compilateurs C proposent une façon de contourner ce problĂšme. Les compilateurs Microsoft Visual C++ et Borland/Embarcadero C++ suppriment la virgule indĂ©sirable, ignorant ce qui est Ă©crit dans la norme. GCC implĂ©mente `__VA_ARGS__` conformĂ©ment Ă  la norme mais propose pas moins de trois autres options pour obtenir la suppression de la virgule : ```C // Utilisation de ## pour faire « disparaĂźtre » la virgule lorsque __VA_ARGS__ est vide #define LOG(X, ...) fprintf(stderr, X, ## __VA_ARGS__) // Une syntaxe diffĂ©rente qui permet de #define LOG(args...) fprintf (stderr, args) // __VA_OPT__ (introduit en C++20, mais acceptĂ© aussi dans le code C par gcc) #define LOG(format, ...) fprintf (stderr, format __VA_OPT__(,) __VA_ARGS__) ``` Finalement, la note [N3033](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3033.htm) normalise cette derniĂšre option avec `__VA_OPT__`. Cette note a Ă©tĂ© intĂ©grĂ©e Ă  la fois dans C++20 et C23, afin de conserver une cohĂ©rence entre les deux langages et de ne pas se retrouver avec deux façons diffĂ©rentes de faire la mĂȘme chose. SpĂ©cificateurs de stockage pour les littĂ©raux composites ------------------------------------------------------------------ Le code suivant n’est pas valide (mais il est acceptĂ© sans problĂšme par gcc) : ```C int function(void) { static struct foo x = (struct foo) {1, 'a', 'b'}; } ``` Le « problĂšme » est l’initialisation d’une variable `static` Ă  partir d’une structure qui n’est pas dĂ©clarĂ©e comme une constante. On peut essayer de dĂ©clarer le littĂ©ral composite (la partie Ă  droite du `=`) comme une constante : ```C static struct foo x = (constexpr struct foo) {1, 'a', 'b'}; ``` Ce n’était pas autorisĂ© dans les versions prĂ©cĂ©dentes du C. La note [N3038](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3038.htm) autorise cette notation. Cela est valable pour tous les spĂ©cificateurs de stockage: `constexpr`, `const`, `static` et `thread_local`. Cela permet des simplifications d’écriture pour des choses plus complexes, par exemple, le code suivant : ```C // DĂ©clare un pointeur statique sur une stucture elle-mĂȘme statique static struct foo* p = &(static struct foo) {1, 'a', 'b'}; ``` remplace, de façon plus compacte, ce qu’il fallait Ă©crire dans les versions prĂ©cĂ©dentes du C : ```C static struct foo Unique = (struct foo) {1, 'a', 'b'}; static struct foo* p = &Unique; ``` constexpr --------- Puisqu’on parle de `constexpr`, il s’agit encore une fois d’un mot-clĂ© rĂ©cupĂ©rĂ© du C++ (du C++11 pour ĂȘtre prĂ©cis). L’idĂ©e est de permettre de dĂ©clarer des constantes dans d’autres types que `int`. On pourrait penser que `const` serait suffisant pour dĂ©clarer des constantes, mais ce n’est pas exactement le cas. Une variable avec un type `const` ne peut pas ĂȘtre modifiĂ©e, mais il s’agit tout de mĂȘme d’une variable. En particulier cela signifie que le code suivant est valide : ```C const int a = 47; const int* b = &a; // a est une variable, elle est stockĂ©e quelque part en mĂ©moire et a donc une adresse. ``` Avec `constexpr`, ajoutĂ© par la note [N3018](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3018.htm) on a vĂ©ritablement une constante qui n’est pas une variable : ```C constexpr int a = 47; constexpr int* b = &a; // interdit: une constante n’a pas d’adresse // de la mĂȘme façon que c’est interdit d’écrire: constexpr int* c = &47; ``` Cela a des consĂ©quences par exemple sur l’allocation des tableaux : ```C const int a = 47; int b[a]; // crĂ©e un tableau de taille variable (VLA) static int c[a]; // interdit : a n’est pas une constante et un tableau static ne peut pas ĂȘtre de taille variable ``` ```C constexpr int a = 47; int b[a]; // crĂ©e un tableau de taille fixe, 47 Ă©lĂ©ments static int c[a]; // crĂ©e un tableau de taille fixe ``` La note [N2713](https://www.open-std.org/JTC1/SC22/WG14/www/docs/n2713.htm) clarifie cependant que le compilateur peut choisir, dans le cas particulier des tableaux, que le compilateur est autorisĂ© Ă  aller plus loin que la dĂ©finition stricte d’une expression s’il arrive Ă  calculer une valeur lors de la compilation. Mais on ne peut pas ĂȘtre sĂ»r que tous les compilateurs seront d’accord sur le sujet. Le nouveau mot clĂ© `constexpr` ouvre Ă©galement la possibilitĂ© de dĂ©clarer des constantes qui ne sont pas des types primitifs. Auparavant, la seule façon d’avoir une constante en C Ă©tait d’utiliser une valeur littĂ©rale : ```C 1234 // une constante de type int 1234U // une constante de type unsigned int 1234LU // une constante de type long unsigned int ``` Impossible d’utiliser cette façon de faire pour dĂ©clarer une constante de type `uint16_t` ou `size_t` de façon portable, par exemple. Ce code peut donc ĂȘtre compilĂ© sans erreur : ```C const uint16_t entier = 0x10000; // il faudrait 17 bits, mais le compilateur ne voit pas de problĂšme Ă  faire une conversion implicite. constexpr uint16_t entier = 0x10000; // ici le compilateur peut gĂ©nĂ©rer une erreur ``` Les possibilitĂ©s de `constexpr` en C s’appliquent uniquement aux valeurs littĂ©rales et Ă  la dĂ©claration de constantes. Cela est beaucoup plus limitĂ© que le mot-clĂ© de C++, qui peut quant Ă  lui ĂȘtre utilisĂ© dans des paramĂštres de fonctions, ou dans des _templates._ Meilleure gestion des nombres en binaire ---------------------------------------- En C, on peut utiliser trois bases diffĂ©rentes pour Ă©crire une valeur entiĂšre : ```C int decimal = 10; // En dĂ©cimal int octal = 010; // En octal, reprĂ©sente la valeur 8 en dĂ©cimal int hex = 0x10; // En hexadĂ©cimal, reprĂ©sente la valeur 16 en dĂ©cimal ``` Il manquait le binaire. La note [N2549](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2549.pdf) le rajoute, avec le prĂ©fixe `0b` : ```C int binaire = 0b10; // ReprĂ©sente la valeur 2 en dĂ©cimal ``` La note [N2630](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2630.pdf), quant Ă  elle, ajoute de nouveaux spĂ©cificateurs de format pour `printf`, `scanf` et les fonctions associĂ©es pour permettre l’affichage et la lecture de nombres en binaire : ```C printf("La valeur %d s’écrit en hexa: %#x et en binaire: %#b\n", valeur, valeur, valeur); ``` La note [N3022](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3022.htm) apporte une façon standardisĂ©e de connaĂźtre le boutisme du systĂšme (stockage des nombres en commençant par l’octet de poids fort ou de poids faible), ainsi que des fonctions de manipulation binaire (rotation de bits, comptage du nombre de bits Ă  1 ou Ă  0 dans un nombre, etc). Ces fonctions Ă©taient dĂ©jĂ  disponibles pour la plupart dans gcc sous un autre nom (par exemple la fonction `__builtin_clz` de gcc est Ă©quivalente Ă  `stdc_leading_zeros`). Cependant, ces opĂ©rations ne permettent toujours que de travailler avec des entiers de taille « classique »: 8, 16, 32 ou 64 bits. Que faire si on a besoin d’une autre taille ? Dans ce cas il faut utiliser les ajouts de la note [N2709](https://www.open-std.org/jtc1/sc22/WG14/www/docs/n2709.pdf), qui permet de dĂ©clarer des entiers de taille variable: ```C int unEntier = 12; // un entier signĂ©, avec un nombre de bits non normalisĂ© (rien de nouveau ici) _BitInt(4) unAutreEntier = 5; // un entier signĂ© sur 4 bits const unsigned _BitInt(4) encoreUn = 5; // un entier non-signĂ© const sur 4 bits _BitInt(4)* unPointeur = &unAutreEntier; // cela fonctionne comme n’importe quel autre type ``` La note [N2775](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2775.pdf) ajoute les suffixes « wb » et « uwb » pour les littĂ©raux, ce qui permet de dĂ©clarer des littĂ©raux de type _BitInt. En particulier c’est utile pour des types avec plus de bits que les types standards, pour lesquels un littĂ©ral classique (mĂȘme avec un suffixe `ull`) ne suffirait pas: ```C unsigned _BitInt(128) unGrandEntier = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFuwb; ``` Chaque implĂ©mentation doit dĂ©finir une valeur pour `BITINT_MAXWIDTH` indiquant quel est le nombre de bits maximal utilisable. Il faudra voir si des entiers de 128 bits (ou plus) sont autorisĂ©s, ce qui serait utile par exemple pour certains algorithmes cryptographiques, ou si les compilateurs choisiront de ne pas autoriser plus que 64 bits. Macros pour les calculs avec vĂ©rification de dĂ©bordement -------------------------------------------------------- En C, il n’y a pas de vĂ©rification de dĂ©bordement sur les entiers: ```C uint32_t a = 0xFFFFFFFF; uint32_t b = 0xFFFFFFFF; uint32_t c = a + b; // pas d’errreur, ni Ă  la compilation ni Ă  l’exĂ©cution ``` La note [N2683](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2683.pdf) ajoute des macros permettant de faire des calculs avec vĂ©rification d’erreurs : ```C #include uint32_t a = 0xFFFFFFFF; uint32_t b = 0xFFFFFFFF; uint32_t c; if (ckd_add(&c, a, b)) { // il y a un overflow. Le contenu de c est Ă©gal au rĂ©sultat modulo 32. } ``` Les deux macros `ckd_sub` (pour les soustractions) et `ckd_mul` (pour les multiplications) fonctionnent de la mĂȘme façon. Il est possible d’utiliser des types diffĂ©rents pour les 3 paramĂštres, y compris les `_BitInt` prĂ©sentĂ©s dans le paragraphe prĂ©cĂ©dent. Cette syntaxe est assez lourde, surtout si on veut rĂ©aliser plusieurs opĂ©rations (`a = b * c + d` par exemple). La note 2683 propose des amĂ©liorations dans des paragraphes _future directions_ et des annexes de la norme. On peut donc s’attendre Ă  des Ă©volutions dans les prochaines versions du langage. Nombres dĂ©cimaux Ă  virgule flottante ------------------------------------ Le langage C ne spĂ©cifie pas exactement comment sont traitĂ©s les nombres Ă  virgule flottante (float, double, et long double). Lorsque les premiĂšres versions du C ont Ă©tĂ© Ă©crites, il n’existait pas de standard pour cela, et les fabricants de processeurs (ou les dĂ©veloppeurs de compilateurs, si le processeur n’avait pas de matĂ©riel dĂ©diĂ© aux calculs flottants) pouvaient dĂ©velopper leur propre format. Cela a changĂ© depuis, et on peut au moins supposer que les types utilisĂ©s sont basĂ©s sur une virgule flottante binaire, c’est-Ă -dire que les nombres sont encodĂ©s sous la forme : (−1)sign x significand x 2^exponent - 1 bit de signe (positif ou nĂ©gatif) - une mantisse comprise entre 0 et 1 - un exposant (positif ou nĂ©gatif), indiquant par quelle puissance de 2 il faut multiplier le nombre L’avantage de cette approche est que la multiplication par une puissance de 2 est facile Ă  rĂ©aliser (il s’agit d’un simple dĂ©calage de bits vers la gauche ou vers la droite). L’inconvĂ©nient est que certaines valeurs dĂ©cimales simples ne peuvent pas ĂȘtre reprĂ©sentĂ©es de façon correcte. Par exemple, le nombre dĂ©cimal 0.1 serait reprĂ©sentĂ© par 3602879701896397 * 2^-55 = 0.1000000000000000055511151231257827021181583404541015625. Il y a donc des erreurs d’arrondi lĂ  oĂč on ne s’y attend pas en rĂ©flĂ©chissant en nombres dĂ©cimaux. Une solution est l’utilisation de nombres flottants dĂ©cimaux, plus faciles Ă  manipuler pour les humains mais moins pour les ordinateurs. C’est ce que propose la note [N1724](https://www.open-std.org/jtc1/sc22/WG14/www/docs/n1724.pdf) avec les nouveaux types `_Decimal32`, `_Decimal64` et `_Decimal128` (ce dernier Ă©tant optionnel). Avec ces types les nombres sont reprĂ©sentĂ©s sous la forme: (−1)sign x significand x 10^exponent (−1)sign x significand x 10^exponent L’ajout de ces types s’accompagne de nombreuses mises Ă  jour dans la bibliothĂšque standard: les fonctions de formatage (printf, scanf...) doivent pouvoir afficher et dĂ©coder ces nombres, les fonctions mathĂ©matiques (trigonomĂ©trie, logarithmes...) doivent pouvoir les manipuler. Les opĂ©rateurs classiques (+, -, =...) doivent fonctionner avec. De nombreuses constantes sont Ă©galement ajoutĂ©es (pour les valeurs minimales et maximales qui peuvent ĂȘtre reprĂ©sentĂ©es, par exemple). On peut Ă©galement dĂ©clarer des variables de ces types: ```C _Decimal32 dixieme = 0.1df; ``` L’apparition de ces types entraĂźne l’apparition de fonctions pour les manipuler. Par exemple, pour calculer un cosinus dans diffĂ©rents types, on dispose de: ```C float cosf(float v); double cos(double v); _Decimal32 cosd32(_Decimal32 v); _Decimal64 cosd64(_Decimal64 v); _Decimal128 cosd128(_Decimal128 v); ``` Ce changement s’applique Ă  toutes les fonctions manipulant des nombres Ă  virgule flottante, par exemple les fonctions de manipulation d’exposants : `quantizedN`, `samequantumdN`, `quantumdN`, `llquantexpdN`, ou les fonctions de formatage : `encodedecdN`, `decodedecdN`, `encodebindN`, `decodebindN`, `strfromdN` et de conversion de chaĂźnes en entier: `strtodN`. Enfin, les fonctions de formatage (de la famille `printf` et `scanf`) peuvent Ă©galement manipuler ces nombres, grĂące aux spĂ©cificateurs `"%H"`, `"%D"`, et `"%DD"` pour les nouveaux types dĂ©cimaux flottants _Decimal32, _Decimal64, et _Decimal128 respectivement. ```C _Decimal32 d1; _Decimal64 d2; _Decimal128 d3; printf("Voici des nombres: %H, %D, %DD\n", d1, d2, d3); ``` La note [N2341](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2341.pdf) donne un bon aperçu de toutes les modifications liĂ©es aux nombres flottants. La prise en charge d’Unicode ---------------------------- ### Le type char8_t pour l’UTF-8 Historiquement en C, les chaĂźnes de caractĂšres sont stockĂ©es en utilisant le type `char`. Ce type est prĂ©vu pour reprĂ©senter des caractĂšres en ASCII, et donc sur seulement 7 bits. Par consĂ©quent, certains compilateurs (et certaines architectures de processeur) choisissent que char peut ĂȘtre un type signĂ© ou non signĂ©. Au dĂ©part, l’utilisation d’un type signĂ© permettait d’avoir des fonctions retournant soit un caractĂšre ASCII valide, soit -1 en cas d’erreur. Mais par la suite, cela a posĂ© beaucoup de problĂšmes pour utiliser d’autres encodages de caractĂšres. Certaines fonctions ont Ă©tĂ© modifiĂ©es pour utiliser un int plutĂŽt qu’un char afin de limiter les problĂšmes. De plus, le type char utilise en principe l’encodage « natif » du systĂšme, dans le cas d’un systĂšme UNIX cela dĂ©pend donc de la locale choisie par l’utilisateur (ou par le programme lui-mĂȘme avec la fonction `setlocale()`). Le comportement d’un programme peut donc varier en fonction de la locale, en plus de l'architecture du processeur et du compilateur. Il devient rapidement trĂšs complexe de penser Ă  tous les cas. Enfin, les fonctions classiques du C pour caractĂ©riser un caractĂšre (isalpha, isdigit...) ne peuvent pas fonctionner avec du texte encodĂ© en UTF-8 oĂč un seul caractĂšre peut ĂȘtre encodĂ© sur plusieurs octets. La note [N2653](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2653.htm) introduit un nouveau type char8_t qui est dĂ©diĂ© au stockage de chaĂźnes en UTF-8. Ce type est forcĂ©ment non signĂ©. De plus, les littĂ©raux prĂ©fixĂ©s par u8 sont maintenant de ce type (c’étaient auparavant des `unsigned char`): ```C char8_t* une_chaine = u8"CaractĂšres accentuĂ©s encodĂ©s en UTF-8"; char* une_autre = "CaractĂšres accentuĂ©s encodĂ©s selon la 'locale' active"; ``` Le type char8_t est Ă©galement disponible en C++, avec quelques diffĂ©rences pour garder une correspondance avec les types char16_t, wchar_t et char32_t qui ont Ă©tĂ© normalisĂ©s de façon un peu diffĂ©rente dans les deux langages. La note ajoute Ă©galement les fonctions `mbrtoc8()` et `c8rtomb()` qui s’utilisent comme les fonctions Ă©quivalentes pour les autres types « wide char » et permet de convertir les chaĂźnes entre les diffĂ©rentes reprĂ©sentations. Enfin, un type `atomic_char8_t` est Ă©galement ajoutĂ©, si jamais vous avez besoin d’opĂ©rations atomiques (non interruptibles) sur les caractĂšres. ### Changements du comportement de \u La note [N2828](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2828.htm) modifie la sĂ©quence d’échappement \u utilisĂ©e pour entrer des caractĂšres unicode dans une chaĂźne littĂ©rale: ```C const char meow[] = u8"\U49584958"; ``` Le code ci-dessus essaie de dĂ©finir un caractĂšre Unicode de 32 bits. En rĂ©alitĂ©, la norme ISO 10646 n’autorise pas plus de 21 bits (ce qui permet d’encoder le caractĂšre en UTF-8 sur 4 octets maximum). La note interdit donc officiellement les caractĂšres dĂ©passant cette limite (c’était dĂ©jĂ  le cas dans certains compilateurs). SĂ©parateur de chiffres dans les constantes ------------------------------------------ La Note [N2626](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2626.pdf) est encore une fonction empruntĂ©e au C++ (C++14 pour ĂȘtre prĂ©cis): les nombres littĂ©raux peuvent ĂȘtre Ă©crits avec un sĂ©parateur entre les chiffres: ```C uint32_t a = 0xFFFF'FFFF; int b = 1'000'000; uint16_t c = 0b1111'1111'1111'1111; ``` Cela apporte plus de lisibilitĂ©, en particulier quand il y a beaucoup de chiffres. Les apostrophes peuvent ĂȘtre placĂ©es oĂč on veut, selon ce qu’on veut mettre en valeur. typeof() -------- `typeof` permet d’obtenir le type d’une variable. Une utilisation classique pourrait ĂȘtre : ```C // Une macro pour Ă©changer deux valeurs qui ne fonctionne que pour les entiers : #define echange_entier(a, b) { int tmp = a; a = b; b = tmp; } // Une macro adaptĂ©e pour fonctionner avec n’importe quel type : #define echange(a, b) { typeof(a) tmp = a; a = b; b = tmp; } ``` `typeof` est dĂ©jĂ  implĂ©mentĂ© par la plupart des compilateurs C, mais n’était pas encore normalisĂ©. C’est maintenant le cas avec la note [N2927](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2927.htm). Les utilisateurs de C++ ont peut-ĂȘtre remarquĂ© que le mot-clĂ© choisi n’est pas le mĂȘme qu’en C++, en effet ce dernier utilise `decltype` pour le mĂȘme usage. La raison de cette divergence est une diffĂ©rence de comportement. Imaginons ceci dans un fichier .h qui peut ĂȘtre utilisĂ© Ă  la fois par du code C et du code C++ : ```C int a; typeof(a) // int en C decltype(a) // int en C++, jusque-lĂ  tout va bien... // mais si on ajoute un peu trop de parenthĂšses: decltype((a)) // int& en C++ (rĂ©fĂ©rence sur un entier) typeof((a)) // toujours un int en C, bien entendu ``` Si les deux opĂ©rations avaient le mĂȘme nom, on se retrouverait avec des types diffĂ©rents selon que le code est compilĂ© en C ou en C++. Une proposition a Ă©tĂ© soumise au commitĂ© de normalisation du C++ pour ajouter `typeof`, avec une implĂ©mentation Ă©quivalente Ă  `std::remove_reference_t`. La fonction call_once devient obligatoire ----------------------------------------- `call_once` a Ă©tĂ© introduite en C11 dans la partie concernant les threads. L’idĂ©e est de s’assurer qu’une fonction est appelĂ©e une seule fois, y compris lorsqu’il y a plusieurs threads. ```C void initialise(void) { // Initialisation qui ne sera faite qu’une fois } int fonction(void) { static once_flag flag = ONCE_FLAG_INIT; call_once(&flag, initialise); } ``` Cependant, cette construction n’est pas nĂ©cessairement limitĂ©e aux programmes utilisant plusieurs threads. L’utilisation de la fonction ci-dessus peut trĂšs bien se faire plusieurs fois depuis le mĂȘme thread. La note [N2840](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2840.htm) dĂ©place donc `call_once` dans la norme pour la rendre obligatoire y compris pour les implĂ©mentations du C ou il n’y a pas de threads. Cela fournit un pendant Ă  `atexit`, qui peut ĂȘtre utilisĂ© pour la dĂ©-initialisation des ressources lors de l’arrĂȘt du programme. unreachable() ------------- La note [N2826](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2826.pdf) ajoute la fonction `unreachable()` qui permet d’indiquer qu’une branche du code ne peut pas ĂȘtre atteinte. Cela ressemble un peu Ă  `assert()`, mais cette derniĂšre peut ĂȘtre dĂ©sactivĂ©e avec `#define NDEBUG`, et dans ce cas, il n’y a plus d’indication au compilateur que le code n’est pas supposĂ© ĂȘtre atteignable : ```C int fonction(int param) { assert(param> 0); // dans le code Ă©crit ici, le compilateur peut supposer que param est positif, // sauf si le code est compilĂ© en mode NDEBUG. Les versions "debug" et "release" // ont donc peut-ĂȘtre un comportement diffĂ©rent } int fonction2(int param) { if (param <= 0) unreachable(); // dans le code Ă©crit ici, le compilateur peut supposer que param est positif, // dans tous les cas } ``` __has_include (comme en C++) ---------------------------- Les Ă©volutions de la bibliothĂšque standard (ou d’autres bibliothĂšques) rajoutent parfois de nouveaux fichiers .h. Dans certains cas il est souhaitable d’écrire du code pouvant utiliser ces fichiers seulement s’ils sont disponibles. Historiquement, il fallait pour cela passer par un test avant la compilation (par exemple avec un script « configure », Ă©ventuellement gĂ©nĂ©rĂ© Ă  l’aide des autotools). Le C++17 a ajoutĂ© `__has_include` qui permet de tester la prĂ©sence d’un fichier directement depuis le prĂ©processeur. Le C23 intĂšgre cette mĂȘme fonctionnalitĂ© avec [N2799](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2799.pdf) ```C #if __has_include() #include #define have_optional 1 #elif __has_include() #include #define have_optional 1 #define have_experimental_optional 1 #else #define have_optional 0 #endif ``` InfĂ©rence de type ----------------- Normalement, en C, il faut indiquer le type de chaque variable que l’on dĂ©clare : ```C double x = 0; // une variable de type double double y = cos(x); // une autre variable de type double ``` Cela peut rendre le code compliquĂ© par exemple lors de l’écriture de macros ou de code utilisant _Generic : ```C #define swap(a, b) { int tmp = a; a = b; b = tmp; } // macros ne fonctionnant que pour le type int ``` Ce problĂšme a Ă©tĂ© rĂ©glĂ© en C++11 avec le recyclage du mot clĂ© `auto`: ```C auto y = cos(x); // la fonction cos retourne un double, donc y est de type double #define swap(a, b) { auto tmp = a; a = b; b = tmp; } // tmp est du mĂȘme type que a, cette macro peut fonctionner avec n’importe quel type ``` Ce mot clĂ© a dĂ©sormais le mĂȘme usage en C, suite aux notes N3006 et [N3007](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3007.htm). Cela remplace l’utilisation (prĂ©)historique de `auto` pour dĂ©clarer des variables locales dans une fonction: ```C void fonction(void) { static int a; // une variable statique (valeur conservĂ©e entre les appels de la fonction, stockĂ©e dans un endroit fixe en mĂ©moire) auto int b; // une variable automatique, stockĂ©e sur la pile int c; // une variable automatique aussi, stockĂ©e sur la pile } ``` Plus personne n’indiquait dans son code les variables automatiques explicitement. Le C++ avait donc choisi de recycler ce mot-clĂ© dĂ©jĂ  rĂ©servĂ© par le langage mais dĂ©suet. Le C a fait le mĂȘme choix pour assurer plus de compatibilitĂ© avec le C++. Initialisation avec `= {}` ----------- En C, une variable locale dĂ©clarĂ©e sans initialisation n’est pas initialisĂ©e: ```C void fonction(void) { int a; // variable non initialisĂ©e } ``` Il est vivement recommandĂ© de mettre une valeur connue: ```C void fonction(void) { int a = 0; } ``` Cela se complique un peu pour les structures: ```C typedef struct { int premier; int deuxieme; int troisieme; } structure; void fonction(void) { structure a = { 0 }; // initialise tous les champs Ă  0 structure a = { 0, 0, 0 }; // initialise tous les champs Ă  0 structure a = { 0, 0 }; // erreur de compilation: le troisiĂšme champ n’est pas initialisĂ© } ``` Et cela devient encore plus compliquĂ© quand il y a des structures imbriquĂ©es. Certains dĂ©veloppeurs ont parfois renoncĂ© Ă  comprendre l’utilisation de cette initialisation Ă  0 (surtout sur les cas complexes mĂȘlant structures, unions et tableaux) et utilisent un memset : ```C structure a; memset(&a, 0, sizeof(a)); ``` avec le risque de se tromper dans l’ordre des arguments de la fonction memset, ou d’oublier d’initialiser une variable. Parfois Ă©galement des dĂ©veloppeurs choisissent de dĂ©sactiver les avertissements du compilateur sur certaines initialisations. La note [N2900](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2900.htm) introduit une nouvelle syntaxe: ```C structure a = { }; // initialise tous les champs Ă  0 ``` C’était dĂ©jĂ  autorisĂ© par la plupart des compilateurs, maintenant cela fait partie de la norme. Et cela Ă©vite la confusion avec l’initialisation par des valeurs pour chaque champ. Attributs --------- Les attributs permettent de manipuler les alertes du compilateur pour certains cas d’usages. Ils existaient dĂ©jĂ  sous diverses formes non standardisĂ©es, par exemple `__attribute__(())` pour gcc ou `__declspec()` chez Microsoft. DĂ©sormais une nouvelle syntaxe avec deux paires de crochets fait partie du standard : `[[ ... ]]`. À l’intĂ©rieur des crochets, certains attributs sont standardisĂ©s, mais chaque implĂ©mentation peut ajouter les siens avec un systĂšme d’espaces de noms. Par exemple on pourra trouver `[[gnu::always_inline]]` dans du code compilable avec gcc. La macro `__has_c_attribute( attribute-token )` permet de tester la prĂ©sence d’un attribut pour savoir si on peut l’utiliser dans le code. Quelques-uns des attributs standards en C23: - `[[deprecated]]`, permet de dĂ©corer tout un tas d’élĂ©ments du code comme dĂ©prĂ©ciĂ©s. Et de lever une alerte s’ils sont utilisĂ©s. - `[[fallthrough]]`, permet d’annoter dans un `switch`, les successions de `case` considĂ©rĂ©es comme anormales, afin d’éviter les alertes. - `[[maybe_unused]]`, permet d’annoter une variable qui est certainement utilisĂ©e. Par exemple, en fonction de directives de prĂ©processeur, un paramĂštre de fonction peut ne plus ĂȘtre utilisĂ©. Mais reste utile dans une autre configuration. - `[[nodiscard]]`, permet de s’assurer que la valeur retournĂ©e par une fonction est bien utilisĂ©e. Par exemple pour `malloc()`. - `[[noreturn]]`, permet d’indiquer qu’une fonction ne se termine pas. C’est le cas par exemple de `abort()`, `exit()` ou `longjmp()`. Cette syntaxe est la mĂȘme qui a dĂ©jĂ  Ă©tĂ© standardisĂ©e pour C++. Cependant, C11 avait dĂ©jĂ  introduit des attributs avec une syntaxe plus simple, ces derniers deviennent obsolĂštes ([N2764](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2764.pdf)). Il y a quelques subtilitĂ©s : en C11, le mot-clĂ© ajoutĂ© Ă©tait `_Noreturn` (commençant par un _ pour ne pas empiĂ©ter sur des noms de variables, macros, et dĂ©finis dans le code utilisateur). L’en-tĂȘte `stdnoreturn.h` permettait d’utiliser `noreturn` sous forme d’un `#define`. Dans la nouvelle syntaxe, le problĂšme d’espace de noms ne se pose plus, puisque les attributs sont dans un espace de noms sĂ©parĂ©. Cependant, si `stdnoreturn.h` est inclus, le prĂ©processeur va remplacer `noreturn` par `_Noreturn` et ce y compris dans les nouvelles dĂ©clarations. Le C23 accepte donc _Noreturn comme nom d’attribut afin que du code se trouvant dans cette situation continue Ă  compiler. ```c // Fonction noreturn en C11 (notation dĂ©sormais obsolĂšte) _Noreturn void f () { abort(); } // Fonction noreturn en C23 (nouvelle notation) [[noreturn]] void f () { abort(); } ``` Fonction avec paramĂštre anonyme ------------------------------- La dĂ©finition suivante est dĂ©sormais valide en C23 : ```c int f(int, int) { return 7; } ``` Cela permet de dĂ©finir une fonction qui n’utilise pas certains de ses paramĂštres. C’est utile si la fonction doit avoir un nombre spĂ©cifique de paramĂštres, par exemple si elle est utilisĂ©e via un pointeur de fonction. Auparavant cette notation Ă©tait interdite par le standard, il fallait forcĂ©ment nommer les paramĂštres et utiliser un attribut (`maybe_unused`) ou d’autres astuces pour Ă©viter d’avoir un avertissement pour un paramĂštre non utilisĂ©. Tableaux et Ă©lĂ©ments similairement qualifiĂ©s -------------------------------------------- En C, les types peuvent ĂȘtre _qualifiĂ©s_, l’exemple le plus connu est l’utilisation de la qualification `const` pour les constantes : ```C int a; // un entier variable const int b; // un entier constant ``` Des complications apparaissent avec les pointeurs: le pointeur lui-mĂȘme peut ĂȘtre constant (ou pas), et les Ă©lĂ©ments pointĂ©s Ă©galement: ```C const char* c = "Bonjour"; // un pointeur sur une chaĂźne constante c[1] = 'a'; // interdit, le type pointĂ© est constant c = "Au revoir"; // autorisĂ©, le pointeur n’est pas constant donc il peut pointer ailleurs char* const d = strdup(c); // un pointeur constant vers une chaĂźne variable d[1] = 'a'; // autorisĂ©, le type pointĂ© n’est pas constant d++; // interdit, le pointeur est constant et ne peut pas ĂȘtre modifiĂ© const char* const e; // un pointeur constant sur un une chaĂźne constante const char* const * const f; // un pointeur constant sur un pointeur constant sur... et ainsi de suite ``` Cependant, cela ne fonctionne pas pour les tableaux. ```C const int a[] = {1, 2, 3}; // un tableau contenant des entiers constants ``` Il n’y a pas d’endroit oĂč mettre un `const` pour qualifier le tableau lui-mĂȘme. Cela peut se comprendre, car de toutes façons on ne peut pas affecter ou incrĂ©menter un tableau: ```C a = { 5 }; // interdit a++; // interdit aussi ``` Dans des cas un peu plus complexes, on se retrouve avec des choses qui ne fonctionnent pas de façon inattendue : ```C // Une fonction de transposition de matrice. // Le paramĂštre 'in' n’est pas modifiĂ©, il est donc dĂ©clarĂ© 'const' void transpose(int N, int M, double out[M][N], const double in[N][M]) { for (int i = 0; i < N; i++) for (int j = 0; j < M; j++) out[j][i] = in[i][j]; } const double a[2][2] = { ... }; double o[2][2]; transpose(2, 2, o, a); // le tableau a est constant et peut ĂȘtre passĂ© en paramĂštre double b[2][2]; transpose(2, 2, o, b); // type incompatible ``` Ou encore, lors de la conversion d’un tableau en pointeur le const n’est pas conservĂ© comme il faudrait : ``` const int foo[5]; memset(&foo, 0, sizeof foo); // Ă©crit dans un tableau qui devrait ĂȘtre constant ``` La note [N1923](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1923.htm) change ce comportement. DĂ©sormais, le qualificateur s’applique Ă  la fois au tableau et Ă  ses Ă©lĂ©ments: ``` const double a[2][2] = { ... }; // en C23: un tableau constant contenant des const double. ``` Avec une exception: un tableau n’est jamais qualifiĂ© d'`_Atomic`, mais ses Ă©lĂ©ments peuvent l’ĂȘtre. ```c typedef int A[1][2]; const A a = {{4, 5}}; // en C23: un tableau const de tableau const int* b = a[0]; // Erreur depuis C89 (b doit ĂȘtre const) void *ptr = a; // Erreur depuis C23 (ptr doit ĂȘtre const) ``` Changement sur les assertions ----------------------------- Il existe en C deux fonctions permettant de vĂ©rifier que des conditions sont bien vĂ©rifiĂ©es, et d’arrĂȘter le programme dans le cas contraire. Il s’agit de `assert` et `static_assert`. Le premier vĂ©rifie des choses lors de l’exĂ©cution et le second lors de la compilation. Tous les deux ont reçu quelques changements. Tout d’abord le static_assert. Normalement il s’utilise avec en paramĂštre une expression, et un message Ă  afficher par le compilateur : ```C int tableau1[10000]; int tableau2[10000]; static_assert(sizeof(tableau1) == sizeof(tableau2), "Les deux tableaux doivent avoir la mĂȘme taille"); ``` La mĂȘme syntaxe est disponible en C++. Cependant, la derniĂšre version de C++ a rendu optionnel le message, qui n’apporte finalement pas grand-chose la plupart du temps. C23 intĂšgre Ă©galement ce changement et rend le message optionnel. On pourra donc Ă©crire plus simplement : ```C static_assert(sizeof(tableau1) == sizeof(tableau2)); ``` Du cĂŽtĂ© de `assert()`, le problĂšme est qu’il est dĂ©fini sous forme d’une macro et que cette derniĂšre manque de parenthĂ©sage pour protĂ©ger les paramĂštres. Par exemple si on Ă©crit: ```C assert((int[2]){1,2}[0]); ``` Le prĂ©processeur va dĂ©couper les arguments Ă  assert en deux: `(int[2]){1` d’un cĂŽtĂ© et `2}[0]` de l’autre. Ce qui bien sĂ»r se termine par une erreur du compilateur. La note [N2829](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2829.htm) corrige ce problĂšme Ă  la fois pour C et C++. Changement sur les labels ------------------------- En C++, les _labels_ sont utilisĂ©s de deux façons : Avec l’instruction `goto` : ```C void function(void) { int* a = malloc(...); ... if (error) goto end; // continuer l’exĂ©cution au niveau du label "end" ... end: // (ici) free(a); } ``` Et dans les `switch`: ```C switch(a) { case 1: // un autre type de label break; default: break; } ``` Pour des raisons historiques, les cas suivants n’étaient pas autorisĂ©s : ```C void f(int x) { restart: // interdit: label pointant sur une dĂ©claration de variable int z = 3 * x; switch (x) { case 1: // interdit: label pointant sur une dĂ©claration de variable int y = z; if (y == 3) goto out; if (y> z) goto restart; break; default: // interdit: label Ă  la fin d’un bloc } out: // interdit: label Ă  la fin d’un bloc } ``` La note [N2496](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2496.pdf) rend le code ci-dessus valide. Cependant, quelques cas posaient encore problĂšme et la norme a Ă©tĂ© Ă  nouveau modifiĂ©e par [N2508](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2508.pdf) puis par [N2663](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2663.pdf) pour les corriger. Les types entiers Ă  taille exacte peuvent ĂȘtre plus grands que uintmax_t ------------------------------------------------------------------------ En C il existe diffĂ©rents types d’entier, en plus des classiques `int` et `unsigned int`, et de leurs variantes `long` et `short`, on trouve Ă©galement des types Ă  taille exacte, par exemple `int32_t` qui fait 32 bits. La norme dĂ©finit Ă©galement un `uintmax_t` qui doit ĂȘtre le plus grand type disponible. Par exemple, si tous les entiers peuvent ĂȘtre reprĂ©sentĂ©s sur 64 bits, alors `uintmax_t` devrait faire 64 bits. Cela pose problĂšme parce que de nouveaux types peuvent apparaĂźtre dans de nouvelles versions du compilateur. Par exemple, gcc peut compiler du code avec des entiers sur 128 bits, avec un type actuellement nommĂ© `__int128_t`. Mais le type `intmax_t` a Ă©tĂ© dĂ©fini pour ĂȘtre sur 64 bits, et il est utilisĂ© Ă  de nombreux endroits dans le code compilĂ© par gcc. Changer la taille de `intmax_t` provoquerait donc un changement d’ABI et de nombreux problĂšmes de compatibilitĂ©. La note [N2888](https://open-std.org/jtc1/sc22/wg14/www/docs/n2888.htm) autorise donc les compilateurs Ă  dĂ©clarer des types `uintmax_t` et `intmax_t` qui ne sont en fait pas de la taille maximale. Permettant Ă  gcc d’ajouter des entiers sur 128 bits avec le nom `int128_t` sans avoir besoin de changer `intmax_t`. Il faudra rester prudent avec l’utilisation de ces types car `intmax_t` reste le type utilisĂ© par le prĂ©processeur pour manipuler des entiers. Il y a donc un risque d’overflow si on utilise le prĂ©processeur avec des entiers de taille plus large (par exemple la constante `INT128_MAX` dĂ©finie dans `stdint.h`). NouveautĂ©s dans le prĂ©processeur ================================ `#elifdef` et `#elifndef` ------------------------- Le prĂ©processeur C permet de faire de la compilation conditionnelle : ```C #if UNE_MACRO // code compilĂ© si UNE_MACRO ne vaut pas 0 #else // code compilĂ© dans les autres cas #endif ``` On peut aussi tester si une macro est simplement dĂ©finie (mĂȘme si elle vaut 0) ```C #if defined(UNE_MACRO) // code compilĂ© si UNE_MACRO est dĂ©finie #else // code compilĂ© dans les autres cas #endif ``` On peut chaĂźner plusieurs conditions avec `#elif`: ```C #if defined(LINUX) // code spĂ©cifique Ă  Linux #elif defined(BSD) // code spĂ©cifique Ă  BSD #else // code pour les autres systĂšmes #endif ``` Et on peut abrĂ©ger `#if defined` en `#ifdef`: ```C #ifdef(LINUX) // code spĂ©cifique Ă  Linux #elif defined(BSD) // code spĂ©cifique Ă  BSD #else // code pour les autres systĂšmes #endif ``` Il manquait la possibilitĂ© d’abrĂ©ger `#elif defined` en `#elifdef` et `#elif !defined` en `#elifndef`. Ces oublis sont corrigĂ©s par [N2645](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2645.pdf) `#warning` - GĂ©nĂ©rer un avertissement pendant la compilation ------------------------------------------------------------ Il existe en C une directive `#error`: ```C #if sizeof(size_t) < 8 #error "Ce programme a besoin d'un espace d'adressage d'au moins 64 bits" #endif ``` La note [N2686](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2686.pdf) ajoute la directive `#warning`: ```C #if sizeof(size_t) < 8 #warning "Ce programme n'a pas Ă©tĂ© testĂ© sur un systĂšme 32 bits" #endif ``` On peut supposer que `#error` fait Ă©chouer la compilation, cependant, ce n’est pas obligatoire dans la norme C qui ne s’intĂ©resse pas du tout Ă  la façon dont le code va ĂȘtre interprĂ©tĂ© ou compilĂ©, et donc ne peut pas spĂ©cifier le rĂ©sultat d’un Ă©ventuel compilateur. La directive `#warning` Ă©tait dĂ©jĂ  disponible comme extension dans plusieurs compilateurs, dĂ©sormais elle fait officiellement partie du langage C normalisĂ©. `#embed` - Inclure des donnĂ©es binaires dans le fichier gĂ©nĂ©rĂ© -------------------------------------------------------------- La note [N3017](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3017.htm) ajoute une nouvelle directive `#embed` qui permet d’inclure des donnĂ©es binaires directement dans le programme compilĂ©. L’utilisation ressemble un peu Ă  celle de `#include`, par exemple: ```C const unsigned char icon_display_data[] = { #embed "art.png" }; ``` Mais plein d’options sont disponibles, par exemple pour inclure seulement 512 octets provenant d’un fichier : ```C const int please_dont_oom_kill_me[] = { #embed "/dev/urandom" limit(512) }; ``` Les options possibles sont : - limit : dĂ©finit le nombre d’octets Ă  inclure - prefix et suffix : ajoutent des caractĂšres avant et aprĂšs le contenu inclus, seulement si celui-ci est non-vide - if_empty pour ajouter des caractĂšres si le fichier Ă  inclure est vide Il y a Ă©galement un `__has_embed` qui fonctionne comme `__has_include` pour tester si le fichier Ă  inclure est disponible. Une utilisation amusante est pour Ă©crire une _quine_ (un programme qui affiche son propre code source): ```C #include int main(void) { const char* source = { #embed __FILE__ suffix(, '0憆') // On ajoute un '0憆' Ă  la fin pour pouvoir utiliser la chaĂźne de caractĂšres avec puts ;} puts(source); return 0; } ``` Pragmas pour contrĂŽler les arrondis sur les calculs en virgule flottante ------------------------------------------------------------------------ ```C float default_mode = 1.0f / 3.0f; // valeur par dĂ©faut de l’arrondi, non dĂ©finie dans la norme #pragma STDC FENV_ROUND FE_UPWARD float up = 1.0f / 3.0f; // arrondi par au-dessus (le rĂ©sultat sera 0.33....34) #pragma STDC FENV_ROUND FE_DOWNWARD float down = 1.0f / 3.0f; // arrondi par au-dessous (le rĂ©sultat sera 0.33....33) #pragma STDC FENV_ROUND FE_DYNAMIC float dynamic = 1.0f / 3.0f; // arrondi au plus proche ``` Le comportement des arrondis pouvait dĂ©jĂ  ĂȘtre modifiĂ© avec la fonction `fesetround`, mais cela demande pas mal de code pour sauvegarder et restaurer l’environnement (avec `fegetenv` et `fesetenv`) autour de chaque opĂ©ration qui a besoin d’un mode spĂ©cifique. L’utilisation de pragmas permet au compilateur de savoir quelles parties du code utilisent quel type d’arrondi, et d’optimiser les sauvegardes et changements de modes pour ne conserver que ceux qui sont nĂ©cessaires. Les nouvelles fonctionnalitĂ©s de la libc ====================== Fonctions mathĂ©matiques protĂ©gĂ©es contre les dĂ©bordements --------------------------------------------------------- En C, les entiers peuvent dĂ©border, par exemple : ```C uint32_t i = UINT32_MAX; // la plus grande valeur possible pour un entier non-signĂ© sur 32 bits i = i + 1; // maintenant i vaut 0 ``` C’est utile dans certains cas, mais la plupart du temps, c’est une source de problĂšmes. La note [N2683](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2683.pdf) introduit des fonctions permettant de faire des calculs avec vĂ©rification de dĂ©bordement: ```C #include // nouvel en-tĂȘte ajoutĂ© par cette note uint32_t i = UINT32_MAX; if (ckd_add(&i, 1, i) == true) { // dĂ©bordement dĂ©tectĂ© } ``` La note propose d’autres extensions (avec de nouveaux types pour rendre cela plus transparent Ă  l’utilisation) mais pour l’instant ces idĂ©es n’ont pas Ă©tĂ© intĂ©grĂ©es dans le standard. Peut-ĂȘtre dans une prochaine version. PrĂ©servation des *qualifiers* dans les fonctions standard --------------------------------------------------------- Ce changement concerne les fonctions suivantes: `bsearch`, `bsearch_s`, `memchr`, `strchr`, `strpbrk`, `strrchr`, `strstr`, `wcschr`, `wcspbrk`, `wcsrchr`, `wcsstr`, et `wmemchr`. Les valeurs de retour de ces fonctions ne sont pas qualifiĂ©es `const`. Par exemple pour strchr: ```C char *strchr(const char *s, int c); ``` Ceci permet d’utiliser ces fonctions dans deux cas: avec un paramĂštre un buffer constant, ou un buffer qui va ensuite ĂȘtre modifiĂ©. ```C const char* const uneConstante = "Du texte non modifiable"; char* const uneVariable = strdup(uneConstante); // cette chaĂźne peut ĂȘtre modifiĂ©e // Recherche dans une chaĂźne constante, rĂ©sultat constant: const char* const pointeur = strchr(uneConstante, 'n'); // Recherche dans une chaĂźne non constante, le pointeur retournĂ© peut ĂȘtre utilisĂ© pour modifier la chaĂźne: char* const autrePointeur = strchr(uneVariable, 'n'); *autrePointeur = 'b'; // Rien n’empĂȘche d’écrire ceci : char* const dernierPointeur = strchr(uneConstante, 'n'); *dernierPointeur = 'b'; // comportement indĂ©fini: Ă©criture dans de la mĂ©moire constante free(uneVariable); ``` Le problĂšme est corrigĂ© depuis longtemps en C++ qui propose simplement deux prototypes pour les fonctions concernĂ©es : ```C // Si le buffer en entrĂ©e est constant, alors la valeur de retour aussi: const char *strchr(const char *s, int c); // Sinon, les deux sont modifiables: char *strchr(char *s, int c); ``` Le problĂšme est que cette solution exploite la possibilitĂ© en C++ d’avoir plusieurs fonctions avec le mĂȘme nom mais des paramĂštres de types diffĂ©rents. Ce n’est pas possible en C. La note [N3020](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3020.pdf) propose donc une solution diffĂ©rente avec le mĂȘme rĂ©sultat. La note suggĂšre une implĂ©mentation possible: ```C #define IS_POINTER_CONST(P) _Generic(1 ? (P) : (void *)(P) \ , void const *: 1 \ , default : 0) #define STATIC_IF(P, T, E) _Generic (&(char [!!(P) + 1]) {0} \ , char (*) [2] : T \ , char (*) [1] : E) #define _STRING_SEARCH_QP(T, F, S, ...) \ STATIC_IF (IS_POINTER_CONST ((S)) \ , (T const *) (F) ((S), __VA_ARGS__) \ , (T *) (F) ((S), __VA_ARGS__)) #define strstr(S1, S2) _STRING_SEARCH_QP(char, strstr, (S1), (S2)) ``` Cela conduit Ă  quelques acrobaties normatives pour spĂ©cifier exactement le comportement attendu, sans imposer une mĂ©thode d’implĂ©mentation. Et il reste un cas de comportement indĂ©fini si l’argument passĂ© Ă  l’une de ces fonctions est un pointeur `NULL` (le comportement de la macro ci-dessus est d’utiliser l’implĂ©mentation non-const de la fonction, contrairement au C++ ou on obtient normalement une erreur de compilation). Fonctions POSIX --------------- Les fonctions suivantes font leur apparition. Elles Ă©taient dĂ©jĂ  prĂ©sentes dans la spĂ©cification POSIX pour pallier certains problĂšmes des fonctions standards du langage C, mais toutes les implĂ©mentations du C ne ciblent pas des systĂšmes compatibles avec POSIX. - `memccpy()` : copie mĂ©moire d’une zone mĂ©moire vers une autre, pour un nombre d’octets maximal, ou jusqu’à rencontrer un certain caractĂšre (par exemple `0憆`). - `strdup()` : copie une chaĂźne de caractĂšres (terminĂ©e par `null`) dans un espace mĂ©moire allouĂ© automatiquement (et qui doit ĂȘtre libĂ©rĂ© en utilisant `free`). - `strndup()` : comme `strdup`, en rajoutant une contrainte de taille maximale. Ajout de fonctions [rĂ©entrantes](https://fr.wikipedia.org/wiki/R%C3%A9entrance) pour le formatage de date, avec un suffixe `_r`. Elles sont Ă©quivalentes aux fonctions sans suffixe, mais utilisent un espace mĂ©moire passĂ© en paramĂštre Ă  la place d’un espace global unique. Voir : `asctime_r()`, `ctime_r()`, `gmtime_r()`, `localtime_r()`. ```c void compare_year(time_t start, time_t end) { struct tm* start_tm = gmtime(start); struct tm* end_tm = gmtime(end); // ProblĂšme: le deuxiĂšme appel Ă  gmtime va Ă©craser le rĂ©sultat du premier! if (start_tm->tm_year != end->tm_year) { printf("L'annĂ©e a changĂ©"); } } void compare_year_c23(time_t start, time_t end) { struct tm start_tm; gmtime_r(start , &start_tm); struct tm end_tm; gmtime_r(end, &end_tm); // Pas de problĂšme, les deux struct tm sont bien distinctes if (start_tm.tm_year != end.tm_year) { printf("L'annĂ©e a changĂ©"); } } ``` Format alternatif pour les mois dans strftime --------------------------------------------- Les fonctions `strftime()` et `wcsftime()` permettent en C23 d’utiliser les formats `%Ob` et `%OB` pour le formatage des mois (en version abrĂ©gĂ©e et en version longue). Ceci permet par exemple de formater la date correctement dans tous les cas en russe et en ukrainien, dont la grammaire a besoin de deux formes (nominatif et gĂ©nitif) pour les noms de mois en fonction du contexte. Dix annĂ©es d’efforts des dĂ©veloppeurs de la glibc pour [modifier la grammaire de toutes les langues concernĂ©es](https://sourceware.org/bugzilla/show_bug.cgi?id=10871) ayant Ă©chouĂ©, la complexitĂ© de ces langues est donc prise en compte (de façon limitĂ©e) avec cette solution. Le prĂ©fixe O Ă©tait dĂ©jĂ  standardisĂ© pour l’utilisation dans d’autre cas dans le formatage de dates, mais pas pour les noms de mois. timespec_getres() ----------------- En plus des changements listĂ©s ci-dessus sur les fonctions existantes de gestion du temps, la note [N2417](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2417.pdf) ajoute une nouvelle fonction `timespec_getres()`, qui permet de connaĂźtre la rĂ©solution (prĂ©cision) des valeurs retournĂ©es par `timespec_get()`. Cette note apporte Ă©galement de nombreux changements sur d’autres fonctions liĂ©es Ă  la gestion du temps: modification des prototypes de ces fonctions pour interdire le passage accidentel d’un pointeur nul, dĂ©prĂ©ciation de la fonction historique `clock`, et diverses clarifications dans la spĂ©cification. Extensions pour la famille des fonctions `fscanf()` et `fprintf()` ------------------------------------------------------------------ La version C99 de la norme avait ajoutĂ© des types entiers avec des tailles spĂ©cifiques. Historiquement, le C proposait les types `int`, `short` et `long` dont le nombre de bits est dĂ©terminĂ© en fonction de ce qui est raisonnable sur l’architecture matĂ©rielle ciblĂ©e. En C99, des types comme `uint8_t` sont apparus (uint8_t est un entier non-signĂ© occupant exactement 8 bits). Ces types sont optionnels et ne sont prĂ©sents que si l’architecture matĂ©rielle permet de les implĂ©menter efficacement. L’utilisation de ces types avec les fonctions de formatage (`printf` ou `scanf` par exemple) posait problĂšme parce que ces fonctions ont besoin d’une chaĂźne de caractĂšres dĂ©crivant le type Ă  formater. Par exemple : ```C int unEntier = 0; printf("un entier: %d\n", unEntier); unsigned unEntierNonSigne = 0; printf("un entier non signĂ©: %u\n", unEntierNonSigne); long unGrosEntier = 0; printf("un gros entier: %ld\n", unGrosEntier); ``` La solution retenue pour les types Ă  taille fixe Ă©tait d’utiliser des macros, remplacĂ©es par le compilateur par la bonne chaĂźne de formatage : ```C int8_t unEntier8Bits; printf("un entier sur 8 bits: %" PRId8 "\n", unEntier8Bits); // la macro PRId8 est remplacĂ©e par "d" par exemple int32_t unEntier32Bits; printf("un entier sur 32 bits: %" PRId32 "\n", unEntier32Bits); // la macro PRId32 est remplacĂ©e par "d" ou "ld" selon que int32 est en fait un int ou un long int ``` Cette solution est peu pratique Ă  utiliser. La note [N2680](https://www.open-std.org/Jtc1/sc22/WG14/www/docs/n2680.pdf) introduit un nouveau systĂšme de formatage et on pourra dĂ©sormais Ă©crire: ```C int8_t unEntier8Bits; printf("un entier sur 8 bits: %w8d\n", unEntier8Bits); int32_t unEntier32Bits; printf("un entier sur 32 bits: %w32d\n", unEntier32Bits); ``` Cela permettra de corriger quelques surprises lors de l’utilisation des macros prĂ©cĂ©dentes : ```C printf("un entier sur 8 bits: %" PRId8 "\n", 0xFF); // affiche 255 Ă  cause des rĂšgles de promotion de type printf("un entier sur 8 bits: %w8d\n", 0xFF); // affiche -1 ``` Le prĂ©fixe wf (au lieu de seulement w) est Ă©galement disponible pour la famille de types `int_fastN_t`. Macro constantes indiquant la largeur des types entiers ------------------------------------------ Ces constantes sont ajoutĂ©es par la note [N2412](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2412.pdf) suite Ă  la normalisation de l’utilisation du complĂ©ment Ă  2 pour reprĂ©senter les entiers signĂ©s (auparavant, la norme ne spĂ©cifiait pas de reprĂ©sentation spĂ©cifique et ne pouvait donc pas dire grand-chose de la reprĂ©sentation interne des nombres). Il s’agit des constantes `INT8_WIDTH`, `INT16_WIDTH`, `INT32_WIDTH`, `INT64_WIDTH` de leurs Ă©quivalents `INT_FASTN_WIDTH` et `INT_LEAST8_WIDTH` pour les types int_fastN_t et int_leastN_t, ainsi que `INTPTR_WIDTH` et `INTMAX_WIDTH`, indiquant chacune le nombre de bits dans le type concernĂ©. Elles rejoignent CHAR_WIDTH qui existait dĂ©jĂ  dans les versions prĂ©cĂ©dentes du C. Nettoyage de donnĂ©es sensibles ------------------------------ Les donnĂ©es sensibles telles que les mots de passe, devraient ĂȘtre nettoyĂ©es de la mĂ©moire dĂšs qu’elles ne sont plus nĂ©cessaires. La fonction `memset_explicit()` a Ă©tĂ© normalisĂ©e afin de pouvoir exprimer ce besoin ([N2897](https://open-std.org/jtc1/sc22/wg14/www/docs/n2897.htm)). Ainsi le compilateur ne risque plus d’optimiser cette portion de code et de mettre de cĂŽtĂ© le nettoyage de la zone mĂ©moire concernĂ©e. ```c void erase_password(char* password, size_t size) { memset_explicit(password, 0, size); // l’utilisation d’un memset simple ici pourrait ĂȘtre optimisĂ©e par le compilateur : l’espace mĂ©moire est libĂ©rĂ©e juste aprĂšs, donc toute Ă©criture dedans est inutile du point de vue du fonctionnement du programme. free(password); } ``` La trĂšs similaire fonction `memset_s` avait dĂ©jĂ  Ă©tĂ© ajoutĂ©e dans la norme C11, mais dans l’annexe K, dont l’implĂ©mentation n’est pas obligatoire. La note [N1967](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1967.htm) fait un retour d’expĂ©rience sur l’utilisation des fonctions de l’annexe K. Macro pour tester les fonctionnalitĂ©s ------------------------------------- Depuis C95, la macro `__STDC_VERSION__` permet de savoir, Ă  la prĂ©compilation, sur quelle version de la norme le code peut se reposer. La macro est Ă©tendue avec l’annĂ©e de parution et la rĂ©vision de la norme en dĂ©cimal dans un `long`. ```c #if defined(__STDC_VERSION__) && __STDC_VERSION__>= 199409L puts("Le compilateur implĂ©mente C95 ou une version plus rĂ©cente."); #elif defined(__STDC__) puts("Le compilateur implĂ©mente C89."); #endif ``` C23 Ă©tend ce concept Ă  certaines bibliothĂšques standards. Elles peuvent ainsi avoir leur propre cycle de vie. Par exemple les constantes suivantes sont dĂ©finies avec l’identifiant de la norme : - `__STDC_VERSION_FENV_H__` - `__STDC_VERSION_MATH_H__` - `__STDC_VERSION_STDINT_H__` - `__STDC_VERSION_STDLIB_H__` - `__STDC_VERSION_TGMATH_H__` - `__STDC_VERSION_TIME_H__` Ces fichiers d’en-tĂȘte sont en gĂ©nĂ©ral fournis par la bibliothĂšque C et ne sont pas forcĂ©ment synchronisĂ©s avec le compilateur. Il est donc, en thĂ©orie, possible d’obtenir un systĂšme ou la version du C utilisĂ©e par le compilateur n’est pas la mĂȘme que celle implĂ©mentĂ©e par la bibliothĂšque standard. Ces macros permettront donc de vĂ©rifier plus finement la version de chacun des composants. Cela permettra Ă©galement de publier des spĂ©cifications pour le langage C dĂ©coupĂ©es en plusieurs standards sĂ©parĂ©s, pour mettre Ă  jour seulement un ou quelques-uns de ces fichiers sans modifier la version de tout le langage. Conclusion =========== Cette version du C apporte beaucoup de changements et permet de resynchroniser un certain nombre de fonctionnalitĂ©s avec des Ă©volutions prĂ©sentes depuis dĂ©jĂ  quelques annĂ©es dans C++. Il faut maintenant espĂ©rer que ces Ă©volutions seront rapidement intĂ©grĂ©es dans les compilateurs et utilisĂ©es par les dĂ©veloppeurs. Historiquement, le dĂ©ploiement des nouvelles versions du C est relativement lent. Aujourd’hui il est encore assez courant de trouver des projets dĂ©veloppĂ©s en C99 ou mĂȘme parfois en C89, alors que deux versions plus rĂ©centes du langage sont dĂ©jĂ  disponibles depuis plusieurs annĂ©es.

AltStyle ă«ă‚ˆăŁăŠć€‰æ›ă•ă‚ŒăŸăƒšăƒŒă‚ž (->ă‚ȘăƒȘă‚žăƒŠăƒ«) /