Greboca  

DLFP - Dépêches  -  Nouveautés du langage C dans sa prochaine version C23

 -  20 janvier - 

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.

Sommaire

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 », 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. 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

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 :

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 :

int add(int a, int b)
{
    return a + b;
}

Déclaration de fonctions sans spécifier les paramètres

N2841

Le code suivant compilait sans problème dans les versions précédentes du C, mais en C23 ce ne sera plus le cas :

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

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

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 :

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 :

int foo(...)
{
}

La macro va_start est modifiée en C23 pour ignorer ce deuxième argument.

On pourra donc désormais écrire :

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 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.

// Du code sans trigraphes
#include 

int main(void)
{
   int tab[3];
   return 0;
}
// Le même avec des trigraphes
??=include <stdio.h>

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 :

// Le même en utilisant l’encodage ISO-646-FR
£include <stdio.h>

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). Ils ne sont pas utilisés directement par le langage, mais peuvent être utiles par exemple dans les commentaires de type Doxygen :

/** @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. 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. 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).

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.

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 :

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.

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.

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). 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).

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.

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 :

# define alignas _Alignas
# define alignof _Alignof
# define bool _Bool
# define static_assert _Static_assert
# define thread_local _Thread_local

Avec N2934, 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 :

#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 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.

#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

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.

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 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.

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

Le C99 a ajouté la possibilité de déclarer des tableaux dont la taille n’est pas connue à la compilation :

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:

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.

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:

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:

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 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 apporte donc une nouvelle syntaxe pour pouvoir forcer un type spécifique :

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

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 :

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

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 :

// 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 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) :

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 :

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

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

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 :

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 on a véritablement une constante qui n’est pas une variable :

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 :

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
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 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 l

par pulkomandy, bayo, vmagnin, Yves Bourguignon, Ysabeau, Lawless, xdelatour, Julien Jorge, nico4nicolas, gUI, Cm, alkino, Gil Cot, Christophe G.

DLFP - Dépêches

LinuxFr.org

Unvanquished, 10 ans et Invaincu

 -  21 janvier - 

Le jeu libre Unvanquished a eu 10 ans en 2022. Alors que la version Bêta 54 est en train d’être finalisée pour ce début d’année 2023, c’est l’occasion (...)


10 ans de la mort d'Aaron Swartz - « Libre à vous ! » du 10 janvier 2023 - Podcasts et références

 -  19 janvier - 

Cent soixante-quatrième émission « Libre à vous ! » de l’April. Podcast et programme : Les 10 ans de la mort d’Aaron Swartz (informaticien, militant des (...)


Sortie de 0 A.D. Alpha 26 « Zhuangzi »

 -  Octobre 2022 - 

0 A.D. : Empires Ascendant est un jeu vidéo de stratégie en temps réel (Real Time Strategy, RTS) historique en 3D développé par Wildfire Games. C’est (...)


Pétrolette 1.6 « Canicule »

 -  Septembre 2022 - 

Pétrolette est une page d’accueil de lecture d’actualités, libre et gratuite. Elle est immédiatement utilisable sans inscription avec la même adresse (...)


Perl 5.36.0 est sorti

 -  Septembre 2022 - 

La toute dernière version de Perl, la 5.36.0, est sortie le 28 mai 2022. Vous la retrouverez bientôt dans votre distribution préférée.Perl est un (...)