Journal C23, listes variantes et le turfu

Posté par  (site web personnel) . Licence CC By‑SA.
16
31
mai
2024

Sommaire

Hello again 'nal,

Tu vas sans doute penser que je fais du comique de répétition,

mais là avec toi, je tiens quelque chose : j'ai directement embrayé sur la version suivante de:

variant_list

qui implémente le maximum des plus récentes évolutions du langage C (alias C23).

Pour le coup ça commence vraiment à devenir intéressant.
Ce dont je suis le plus fier est que le code compile désormais sans aucun warning avec la version "15.x staging" de GCC (installable ici pour Debian/Ubuntu, ou ici pour RHEL/Fedora). Cela dit, normalement tout est déjà dans GCC 14. Qu'Arch Linux a sûrement déjà 😉.

Le composant lui-même, une liste hétérogène, peut se révéler très utile ; le but premier reste cependant de "démontrer C23", aux détriments de certaines optimisations.

Je vais les lister ci-après (pas toutes, et avec 2-3 que je n'ai pas -encore ?- utilisées mais qui valent le coup d'oeil).


1) auto

L'inférence de type made in C++ fait son entrée sous une forme affaiblie.

auto v = (Value*) calloc(1, sizeof(Value));  // le compilateur déduit "Value*"

ou

auto l = list_create(0); // le compilateur déduit "List*" par la signature

On peut bien l'utiliser avec des pointeurs, comme ici "Value*" et "List*", contrairement à ce que laisse supposer la spécification ; c'est uniquement la syntaxe "auto* var" qui est interdite.

J'ai dit "affaibli" car cela reste une déduction faite à la compilation, pas à l'exécution. Ça veut dire que ce genre de raffinement p.ex.:

auto var = (list_get_Type(l, idx) == EINTEGER) ? list_grab_int(l, idx) : ... // tester les autres types

qui aurait permis un type de retour variable et donc un "var" de type dynamique, reste impossible… pour l'instant !

2) typeof()

Lié au précédent, et à la compilation également ;
pour éviter les répétions ou d'exposer les détails d'implémentation, on peut faire :

for (typeof(val->idx) i = 0;  i < val->idx - 1;  i++) {  // size_t idx

Tant qu'on sait qu'il est de la famille des entiers, on n'a pas besoin de connaître le type précis de "i" (ici "size_t") pour le définir comme itérateur de notre boucle.

Dans le cas des APIs publiques, j'ai ajouté une petit macro sympa -quoi qu'encore sous-exploitée- qui en démontre un usage possible :

for (TYPEOF(list_length) i = 0;  i < list_length(l);  i++)

et qui récupère le type de retour des fonctions supportant la syntaxe "fonction(nullptr, …) -ce qui nécessite bien sûr d'être pensé en amont !
Et le code de ladite macro, justement, utilise…

3) --VA--OPT--(sym)

Utilisée par exemple comme:

define TYPEOF(F, ...) typeof(F(nullptr __VA_OPT__(,) __VA_ARGS__))

cette nouvelle macro plus générique remplace l'ancienne macro GCC non-standard "##VA_ARGS" ;
elle omet dans notre cas la virgule "," si l'appel n'a qu'un argument (p.ex. "TYPEOF(fn)" -> "fn(nullptr)").

4) nullptr

NULL est historiquement défini comme "void()()" par GCC, mais "(int)0" par G++ et… d'autres compilateurs.
Cela peut avoir des conséquences subtiles sur le code, notamment sur les macros _Generic ajoutées en C11 et donc je fais un usage intensif.
"nullptr" conserve les propriétés de cast universel de NULL, mais normalise sa définition sur celle de C++, distinct à la fois de "void*" et "int".

char* err = nullptr;
if (!err) err = "test";

Avec les macros _Generic justement, il ne faut pas oublier de gérer son type propre "nullptr_t" :

#define list_set(L, I, V) _Generic((V), \
  int:       list_set_int, \               // 0
  void*:     list_set_void, \              // NULL 
  nullptr_t: list_set_nullptr)(L, I, V)    // nullptr

_

5) fallthrough

Avec tous les warnings activés, GCC en émet un quand un "switch() { case: .." n'est pas délimité par un "break;" (ce qui est en général une erreur).
Cette nouvelle annotation permet d'indiquer que c'est bien l'intention du développeur d'enchaîner :

switch (res) {
    case EINVAL:  if (!err) err="Invalid index";  [[fallthrough]];
    case EAGAIN:  if (!err) err="List locked";    [[fallthrough]];
    case EUNDEF:  if (!err) err="Undefined value";
        fprintf(stderr, "[ERR: %s]\n", err);

_

6) nodiscard

Imaginez une fonction "list_empty()" supposée être utilisée ainsi

if (list_empty(l)) {

sauf que le stagiaire a compris tout à fait autre chose :

list_empty(l); // clear list for future re-use

Un warning peut être explicitement ajouté sur ces maladresses :

[[nodiscard("Did not test the return value!")]]
bool list_empty(List* list);

qui sont parfois bien pires : pensez à "list_create()" qui renvoie un pointeur alloué sur le tas, qu'on est censé libérer…

7) "bool" intégré

C'est un point mineur, mais "bool" n'est désormais plus un "#define 1-#define 0",
c'est un type intégré du langage, distinct de "int", et ne nécessitant plus d'inclure "stdbool.h".

L'avantage est entre autres une bonne gestion par les macros _Generic :

errno_t list_add_int(List* list, int i);
errno_t list_add_bool(List* list, bool b);

#define list_add(L, V) _Generic((V), \
    int:    list_add_int, \
    bool:   list_add_bool, \

_

8) Paramètres optionnels

Un paramètre de fonction peut être non-nommé dans une implémentation,
ce qui ne produit plus de warning genre "Paramètre inutilisé":

errno_t _value_get_Type(Value* v, void*) // "void*" pas utilisé
{
    switch (v->t) {
      case T_INTEGER: return EINTEGER;
      case T_BOOLEAN: return EBOOLEAN;
      case T_FLOAT  : return EFLOAT;
      case T_STRING : return ESTRING;
      default       : return EUNDEF;
    }
}

Il y a au moins deux usages intéressants :
- les pointeurs de fonction ; pour p.ex. leur donner une signature identique, mais n'utiliser qu'une partie des paramètres selon le cas ;
-les sélections par macros _Generic comme ici.

9) {} (initialiseur vide)

Ce code propre à GCC et Clang :

 // does not compile with MSVC
struct MyStruct s = {0}; // all bits are 0 except sometimes with "-02,-03"

qui initalise tous les champs à 0 (y compris le "padding" rajouté selon l'architecture),
mais ne fonctionnait pas avec tous les compilateurs ni tous les niveaux d'optimisation (obligeant à recourir p.ex. à "memset()"),
est désormais normalisé sous la forme :

struct MyStruct s = {};  // all bits are 0, on all compilers & levels

_

11) constexpr

On peut désormais définir des expressions constantes déterminées à la compilation.
Alors ça permet des subtilités auxquelles on ne pense pas forcément :

const int a = 5 + 1;
int arr_a[a];      // tableau de taille variable
memset(arr_a, 0, sizeof(arr_a));

constexpr int b = 5 + 1;
int arr_b[b] = {}; // tableau de taille statique à 6

_

12) ckd_*() (dépassement de bornes)

Historiquement, le standard ne définit pas ce qu'il se passe lorsqu'on fait dépasser ses bornes à un nombre :

int i = 2147483647 + 1; // "-214748364" on GCC without "-fwrapv"

cela se contrôlait, mal, avec des flags propres aux compilateurs comme "-fwrapv".
En plus d'être sources d'erreur complexes, cela ne faisait pas très sérieux pour un langage se vantant de sa performance en calculs mathématiques…
On peut désormais contrôler programmatiquement le comportement des dépassements de bornes :

#include <stdckdint.h>

uint32_t res;

if (ckd_add(&res, 2147483647, 1)) {
  fprintf(stderr, "OVERFLOW! Limiting to upper limit...\n");
  res = INT_MAX;   // 2147483647
}

Il est facile, partant de là, de créer des fonctions du genre "check_add()" effectuant des calculs sûrs.


Voilà !
Comme la dernière fois, j'ai également fait une version bibliothèque du composant sous LGPLv3.
Et je n'ai toujours pas mis de Makefile générique 😉.

  • # siite

    Posté par  (site web personnel) . Évalué à 4.

    Toujours pas de tuple en C pour faire des retour multiple ?
    Ou un type pointeur avec null interdit ?

    Est-ce que constexpr fonctionne avec des appels de fonction ? On pourrait précompiler des expressions régulières par exemple.

    "La première sécurité est la liberté"

    • [^] # Re: siite

      Posté par  (site web personnel) . Évalué à 2.

      Pour le nonnull:
      https://stackoverflow.com/questions/45237148/is-attribute-nonnull-standardized-in-c
      depuis C11, t'a la syntaxe int func(char non_null_ptr[static 1]);

      Pour les tuples, tu dois pouvoir faire struct tuple func(void);

      Et regarder ce qu'il y a dans la struct retourné, le problème étant plus que la librairie standard est faite pour retourner -1 ou NULL en cas d'erreurs.

    • [^] # Re: siite

      Posté par  (site web personnel) . Évalué à 3.

      Toujours pas de tuple en C pour faire des retour multiple ?
      Ou un type pointeur avec null interdit ?

      Je n'aime pas être négatif, mais : non et non.
      Le commentaire d'uso donne une bonne préconisation, mais pas sûr que ça te suffise si ton critère est vraiment l"interdit".

      Est-ce que constexpr fonctionne avec des appels de fonction ? On pourrait précompiler des expressions régulières par exemple.

      Pas encore, mais on y réfléchit (page 3) :

      • there was not consensus to add the constexpr specifier for functions, to C23;
      • there was strong consensus to add the full constexpr feature for objects, extended
      operators (element access), and function definitions, in some future version of C after C23.

      • [^] # Re: siite

        Posté par  (site web personnel) . Évalué à 4. Dernière modification le 03 juin 2024 à 10:22.

        Le tuple permet justement d'éviter de déclarer 12 000 struct comme en Go ou Ocaml. C'est super léger. En plus, c'est facile soit d'utiliser les registre temporaire de la fonction en retour ou d'instancier un struct.

        Concernant le pointeur jamais null je parle vraiment d'un type ou il est garanti de partout qu'il n'est jamais à autre chose qu'un pointeur valide (genre comme un type somme avec la valeur null détecté statiquement à chaque utilisation de *). Cela éviterait des bugs et/ou des tests de partout.

        Le toto[static 1] est un garanti offert au compilateur pour optimiser, pas du tout un moyen de vérification.
        " The syntax only denotes a promise to the compiler that the pointed to object will have at least N elements"

        Le constexpr sur les fonctions permettraient beaucoup de choses, comme la fin des macros un peu pourris, la génération de code, une généralisation de la propagation de constante au delà des litéraux, etc…

        "La première sécurité est la liberté"

        • [^] # Re: siite

          Posté par  (site web personnel) . Évalué à 2.

          Quand tu parles de tuple, je suppose que ça irait aussi avec ce pattern:

          typedef struct
          { bool x; int y; } MyStruct;
          
          MyStruct fn() { return (MyStruct){true, 42}; }
          
          auto [a, b] = fn();   // C++
          

          On y est presque, mais pas tout à fait.
          C est un langage très conservateur ; pour ce sucre-là en bas niveau, il ne faut pas s'interdire de regarder vers C++ (ou Rust).

          • [^] # Re: siite

            Posté par  (site web personnel) . Évalué à 4.

            Je bosse dans l'embarqué qui utilise en gros C et gcc le plus souvent. A moins d'avoir le compilo rust capable de lire des .h, il sera plus rapide de mettre un peu de Rust dans le C pour améliorer les choses.

            J'ai cité les trucs qui me manquent le plus quand je suis retourné au C après le Go et Ocaml. (et encore je n'ai pas demander une liste/array dans le langage de base comme Ocaml ou une hashmap :)

            "La première sécurité est la liberté"

            • [^] # Re: siite

              Posté par  (site web personnel) . Évalué à 2.

              A moins d'avoir le compilo rust capable de lire des .h

              Je suppose que tu fais référence à ce genre d'usage ?

              plus rapide de mettre un peu de Rust dans le C pour améliorer les choses.

              Je suis dans l'embarqué aussi, où C++ est quand même assez toléré de mon côté (d'où ma mention) et a l'avantage de l'écosystème installé.
              Rust je pousse pour à mort ; c'est quasi le cas d'usage idéal ! Juste pour les projets déjà installés, il est parfois considéré "trop récent" ou trop peu riche en devs par les décideurs…

              (après si tu réfères à un mélange que je ne connais pas, je suis intéressé !)

              • [^] # Re: siite

                Posté par  (site web personnel) . Évalué à 3.

                Je suppose que tu fais référence à ce genre d'usage ?

                Oui mais l'idée serait plutôt d'avoir une macro [#include_C=toto.h] qui génère la série d'extern pour pouvoir utiliser les BSP par exemple sans avoir un gros boulot manuel de portage.

                On pourrait même imaginer des annotations dans le code C dédié au Rust pour la gestion de vie de la mémoire.

                "La première sécurité est la liberté"

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.