4 points par GN⁺ 2023-10-10 | 1 commentaires | Partager sur WhatsApp
  • En 2023, ma façon d’écrire du code C a beaucoup évolué, et mon style s’est réorganisé autour de noms de types courts, de l’abandon des chaînes terminées par nul, du retour de structures et de la compilation en une seule unité de traduction
  • Les alias courts comme u8, i32, size, s8, ainsi que la suppression de const et de struct, sont des choix destinés à réduire le bruit visuel et la charge cognitive dans les déclarations répétitives
  • Les chaînes sont manipulées non pas comme des chaînes terminées par nul, mais comme des fat pointers s8 avec data et len ; dans les environnements Win32 et UTF-16, c16 et s16 sont aussi utilisés
  • La conception des fonctions privilégie le retour de structure plutôt que les paramètres de sortie, avec un motif où la valeur de retour initialisée à zéro ne reçoit ok qu’au moment du succès
  • Les règles locales lisibles sont privilégiées jusque dans les macros, les assertions, les déclarations Win32 et l’assembleur inline, tout en suivant le style du projet lorsqu’on contribue à d’autres projets

Exprimer l’intention avec des noms de types courts

  • Des alias courts sont utilisés pour les types de base entiers, caractères et tailles de pointeurs
    • Exemples : u8, c16, b32, i32, u32, u64, f32, f64, uptr, byte, size, usize
  • Comme ces noms apparaissent fréquemment dans tout le programme, la concision apporte un bénéfice direct à la lecture et à la revue
  • Le suffixe _t n’est plus utilisé ; il est désormais perçu comme un élément visuellement distrayant
  • Pour le préfixe des types signés, i est préféré à s
    • s est réservé aux noms de types de chaînes
  • Pour le type de taille, size est utilisé plutôt que isize
    • Parce que la taille signée est considérée comme la valeur par défaut la plus importante
    • usize a un usage plus restreint, principalement lors des interactions avec des interfaces externes
  • b32 exprime l’intention d’un « booléen 32 bits »
    • Une taille de mot naturelle est utilisée plutôt que _Bool
    • En pratique, on considère qu’il se trouve souvent dans un registre ou dans le padding d’une structure
    • Quand la mémoire compte réellement, les booléens sont compactés dans une variable flags
  • c16 est le type destiné aux caractères UTF-16 nécessaires sous Win32
    • S’il est basé sur char16_t, cela aide les débogueurs comme GDB à afficher les données comme des caractères
    • Le nom de type officiel de Win32 est wchar_t, mais il est préférable d’expliciter UTF-16
  • u8 est utilisé pour les octets et principalement les données UTF-8, tandis que byte est distingué pour la mémoire brute et comme type d’aliasing spécial
  • Se préoccuper des systèmes ne prenant pas en charge les types à largeur fixe est jugé peu pratique
    • Les noms de types longs comme int_fast32_t sont également considérés comme un gaspillage inutile
  • Lorsqu’un extrait de code est montré isolément, ces alias ne sont pas utilisés seuls
    • Car le lecteur a aussi besoin du typedef pour comprendre le contexte

Règles pour les macros et les assertions

  • Les macros de type fonction utilisent des minuscules
    • Exemples : countof(a), lengthof(s), new(a, t, n)
  • ALL_CAPS reste préféré pour les constantes, mais les minuscules sont considérées comme plus lisibles pour les macros de type fonction
  • Les macros de type fonction posent moins de problèmes d’espace de noms que les macros ordinaires
    • On peut avoir à la fois une macro new() et une variable ou un champ new
    • Car la macro ne se développe pas si elle n’a pas la forme d’un appel de fonction
  • La macro assert pour GCC et Clang utilise la forme while (!(c)) __builtin_unreachable()
  • Cette forme d’assertion évite de devoir séparer les réglages de build
    • Il n’est pas nécessaire d’avoir des définitions distinctes pour les builds debug et release
    • Son activation est contrôlée par la présence de l’Undefined Behavior Sanitizer, c’est-à-dire UBSan
    • libubsan fournit un diagnostic incluant le nom du fichier et le numéro de ligne
    • En build release, cela devient un indice d’optimisation pratique
  • Pour activer les assertions en build release, mettre UBSan en mode trap avec -fsanitize-trap et activer au minimum -fsanitize=unreachable
  • -funreachable-traps pourrait aussi fonctionner en théorie, mais il est cassé dans plusieurs versions récentes de GCC au moment de l’écriture

Ce qui est réduit dans les déclarations

  • const n’est pas utilisé pour les paramètres
    • Il est considéré comme n’ayant pas de rôle pratique pour l’optimisation
    • Aucun cas où il aurait permis, ou aurait pu permettre, de détecter une erreur ne vient à l’esprit
    • Un bon nom de paramètre est jugé suffisant pour documenter un prototype
  • La suppression de const est un changement qui augmente la productivité en réduisant la charge cognitive et le bruit visuel
  • Petite exception : const reste préféré comme indice pour placer les tables statiques en mémoire en lecture seule, près du code
    • Si nécessaire, const est retiré par cast
  • Pour les pointeurs nuls, le littéral 0 est utilisé
    • C’est un style employé depuis environ sept ans
    • Même s’il existe une possibilité théorique de défaut, aucun cas réel n’a été observé dans des centaines de milliers de lignes de code
  • restrict n’est utilisé que lorsque c’est nécessaire
    • Le code est organisé soit pour ne pas utiliser de paramètres de sortie dans les boucles, soit pour éviter complètement les paramètres de sortie
  • inline n’est pas utilisé
    • Parce que tout est compilé comme une seule unité de traduction
  • Toutes les structures sont définies avec typedef
    • Supprimer le mot-clé struct rend le code plus lisible
    • Pour les structures récursives, une déclaration anticipée est placée juste au-dessus, et des noms courts sont utilisés pour les champs
  • Toutes les fonctions sauf le point d’entrée sont déclarées static
    • Parce que la compilation en une seule unité de traduction est présupposée
  • Grâce aux noms de types courts et à la suppression de const et de struct, il est confortable de placer le type de retour et le nom de la fonction sur la même ligne
  • Il y a eu une période où les noms de types étaient écrits en majuscules, mais cela a finalement été abandonné

Les chaînes utilisent s8 au lieu d’être terminées par nul

  • L’un des changements les plus productifs a été de rejeter complètement les chaînes terminées par nul et d’utiliser un type de chaîne s8 avec data et len
  • Structure de s8 :
    • u8 *data
    • size len
  • La macro s8(s) enveloppe un littéral de chaîne C dans une chaîne s8
  • s8 se transmet et se retourne par valeur, comme un fat pointer
  • s8 fonctionne aussi bien comme préfixe de fonction
    • Car les noms de la famille str sont réservés
    • Exemples : s8span, s8equals, s8compare, s8hash, s8trim, s8clone
  • Pour comparer avec un littéral, on utilise une forme comme s8equals(tagname, s8("body"))
  • Une approche consistant à regrouper la taille et le tableau dans une même allocation via un flexible array member a aussi été essayée, mais son manque de flexibilité a été jugé supérieur à ses avantages
  • Il est arrivé de penser qu’un programme simple n’avait pas besoin de type de chaîne, mais ce jugement s’est généralement révélé erroné
  • s16 est aussi utilisé comme type compatible UTF-16
    • Il contient c16 *data et size len
    • L’ajout de u aux littéraux dans la macro n’est pas encore considéré comme pleinement satisfaisant

Retour de structures et méthode d’initialisation

  • Le retour de structure est préféré aux paramètres de sortie
    • Il s’agit en fait d’une façon de retourner plusieurs valeurs, mais sans destructuring
  • L’exemple i32parse(s8) retourne à la fois value, le résultat du parsing, et ok, son état
  • Le coût de copie supplémentaire n’est pas considéré comme un gros problème en pratique
    • Soit la convention d’appel le transforme en paramètre de sortie restrict caché
    • Soit, une fois inliné, le surcoût de retour n’a plus d’importance
  • Cette approche réduit la tentation de signaler les erreurs par des signaux in-band, comme un retour null spécial
  • Le motif préféré consiste à créer en haut de la fonction une valeur de retour initialisée à zéro et à l’utiliser pour tous les return
    • En cas d’erreur, elle est retournée immédiatement dans son état initialisé à zéro
    • Sur le chemin de succès, ok est mis à true juste avant le retour
  • En dehors des données statiques et des macros s8/s16, l’usage des initializers est aussi réduit
    • Les designated initializers sont également évités, au profit d’initialisations par affectation
  • L’initialisation par affectation est lisible, et la présence d’un sequence point entre chaque affectation fournit un ordre explicite
  • Dans les initialisations où l’ordre des appels peut affecter le résultat, comme avec une fonction de génération aléatoire, il n’est pas nécessaire de réfléchir aux différentes valeurs possibles

Déclarations Win32 et assembleur inline

  • __attribute est préféré à __attribute__
    • Le suffixe __ final est jugé excessif et inutile
  • En programmation système Win32, windows.h n’est pas inclus ; les prototypes nécessaires sont écrits directement
    • Le nombre de déclarations et définitions nécessaires est généralement limité
    • Cela réduit le temps de build et encombre moins l’espace de noms
    • Cela s’accorde plus proprement avec les types personnalisés comme u32, b32 et uptr, plutôt qu’avec DWORD, BOOL ou ULONG_PTR
  • Les exemples de déclarations Win32 utilisent la macro W32(r) __declspec(dllimport) r __stdcall
    • Des fonctions comme ExitProcess, GetStdHandle, VirtualAlloc, WriteConsoleA et WriteConsoleW sont déclarées directement
  • En assembleur inline, les parenthèses extérieures sont traitées comme des accolades
    • Comme avec if, un espace est placé avant la parenthèse ouvrante
    • Chaque ligne de constraint commence par deux-points
  • Un exemple permettant de voir ce style dans un petit programme est wordhist.c
  • Un exemple un peu plus grand est asmint.c, une implémentation d’un mini-langage de programmation

1 commentaires

 
GN⁺ 2023-10-10
Avis Hacker News
  • Il semble considérer que #define sizeof(x) (size)sizeof(x) n’a pas besoin de parenthèses externes, mais il existe une exception très mineure.
    Un cast a une priorité plus élevée que la multiplication, donc sizeof(x) * 3 fonctionne sans risque comme (size)sizeof(x) * 3.
    En revanche, dans (size)sizeof(x)[y], l’indexation de tableau s’applique avant le cast, ce qui donne (size)(sizeof(x)[y]) et non ((size)sizeof(x))[y].
    Dans du code réel, on n’indexera pas sizeof(x), mais comme C autorise integer[pointer] avec le même sens que pointer[integer], cette macro peut compiler tout en se comportant mal à cause du manque de parenthèses.
    Plus fondamentalement, j’ai aussi du mal à adhérer à l’idée qu’une taille signée soit préférable. L’auteur dit que les tailles non signées sont une source de défauts, mais le code présenté contient lui aussi un bug qui corrompt la mémoire si count est négatif.
    Avec un entier non signé, un nombre négatif d’éléments n’est pas représentable et, en cas d’overflow, il devient un très grand nombre positif qui sera intercepté par les vérifications existantes. Personnellement, je préfère utiliser des entiers non signés, mais autant que possible via des wrappers de vérification de bornes qui interrompent l’exécution en cas d’overflow.

    • La sémantique de _Bool, au contraire, me plaît.
      Elle permet d’extraire une expression qui fonctionne bien dans if (flags & FLAG_ALLOCATED) vers une variable booléenne comme _Bool need_free = flags & FLAG_ALLOCATED;.
      Quand flags & FLAG_ALLOCATED est défini, ce n’est pas forcément 1 mais n’importe quelle valeur non nulle ; _Bool la normalise à 1. Si on la stocke dans un int, if (need_free) passe, mais if (need_free == true) peut échouer.
      Il y a aussi des inconvénients. Lors d’un refactoring, si l’on ne remarque pas que la conversion implicite vers _Bool faisait quelque chose d’utile, on peut aboutir à du code incorrect comme if ((flags & FLAG_ALLOCATED) == true).
      De plus, lorsqu’on lit une structure depuis le disque ou qu’on remplit des octets arbitraires, si un champ _Bool n’est ni 0 ni 1, il y a un risque de comportement indéfini.
    • En fait, (size)(sizeof(x)[y]) surprendra aussi beaucoup de gens, mais c’est équivalent à (size)(sizeof ((x)[y])).
      sizeof n’est pas une fonction, mais un opérateur unaire, et l’indexation comme les appels de fonction ont une priorité plus élevée que sizeof. C’est pourquoi je préfère mettre une espace après sizeof et n’utiliser des parenthèses autour de l’opérande que lorsque c’est nécessaire.
      https://en.cppreference.com/w/c/language/operator_precedence
      Pour écrire correctement la macro, ce serait #define sizeof(x) ((size)(sizeof (x))).
    • Bien vu. La leçon est que, si une définition de macro ne s’étend pas en un seul token, il faut toujours l’entourer de parenthèses. Les règles de priorité de C sont vraiment complexes.
  • Définir ses propres types me semble aller un peu trop loin.
    Même quelqu’un déjà familier des types C doit apprendre un système particulier supplémentaire pour comprendre un programme. Préciser la taille est justifié, donc je comprends l’usage de uint32_t plutôt que uint.
    Ces types devraient être définis dans un en-tête approprié, et je peux me tromper, n’ayant pas beaucoup pratiqué C récemment.

    • En pratique, le int de C fait 32 bits.
      Pas sur des cibles 16 bits, mais va-t-on vraiment porter un programme de 5 Mo en 16 bits ? Ce genre d’inquiétude n’en vaut généralement pas la peine.
      Le problème, c’est long. Sur certaines machines il fait 32 bits, sur d’autres 64 bits, ce qui prête à confusion. Heureusement, long long fait toujours 64 bits, donc il suffit d’abandonner long.
      char 8 bits, short 16 bits, int 32 bits, long long 64 bits, et c’est réglé. En C, on a gaspillé un temps infini sur la taille de int.
    • L’auteur précise bien qu’il s’agit de son style de codage personnel. Honnêtement, les types standard sont beaucoup trop verbeux, et je me dis que la liste concise qu’il propose aurait été une bonne chose si elle avait été adoptée autrefois.
    • Pour le dire un peu sur le ton de la plaisanterie, une bonne partie de la programmation consiste à gérer les systèmes de types des autres.
      Pour quelqu’un qui utilise souvent C, les abréviations présentées ici sont familières et, pour un système de types personnalisé, c’est assez élégant. Ça rappelle Rust.
    • Ces types existent dans stdint.h.
      Je suis toujours surpris de voir tant de projets recréer péniblement ce fichier.
      Retraduire les types standard sous ses propres noms est pénible pour les lecteurs. Dans un projet C++ autrefois, j’avais demandé pourquoi il y avait tant de typedef pour les collections, les références et les objets composites ; on m’avait répondu que cela rendait le code plus facile à comprendre.
      Plus tard, j’ai vu une antisèche de typedef collée à côté de l’écran de cette personne.
    • Définir ses propres types entiers a du sens sur les plateformes à ressources limitées.
      On voit souvent des types comme dim_t, qui valent 32 ou 64 bits selon l’usage. Même sur des plateformes 64 bits, les structures à pointeurs compressés utilisent souvent des entiers 32 bits.
      Par exemple, si l’on alloue soi-même un tas et qu’on ne stocke que des offsets 32 bits, les charges de travail de moins de 4 Go divisent par deux l’usage mémoire, et la localité du cache s’améliore aussi, ce qui augmente les performances.
  • Abandonner les conventions établies du C pour des préférences personnelles me paraît un peu excessif
    Utiliser u8 ou i32 au lieu de uint8_t ou int32_t économise bien quelques caractères, mais peut semer la confusion quand quelqu’un d’autre lit le code
    Utiliser un type de chaîne personnalisé plutôt que des chaînes terminées par un nul donne aussi l’impression d’augmenter la difficulté de collaboration, quand on pense que le C a été conçu autour de ce type de chaînes
    Écrire directement les prototypes de l’API Win32 sans inclure windows.h peut réduire le temps de compilation, mais cela ressemble à prendre un sentier en forêt alors qu’une autoroute bien entretenue existe. Une bonne partie de tout cela semble relever davantage de préférences personnelles que de code C facile à manipuler par tous

    • u8 ou i32 ne servent pas à économiser des frappes au clavier, mais à réduire la charge perceptive à la lecture
      L’argument du « nombre de frappes » qui revient toujours dans le débat verbosité contre concision est très bancal. L’idée selon laquelle la concision ne serait utile que pour taper plus vite, et que la verbosité serait toujours meilleure pour la lecture, est fausse
      La verbosité a aussi des avantages pour la compréhension, mais la concision en a également, et aucun des deux camps n’est clairement vainqueur. Ce sont simplement des compromis différents
    • Des noms comme u16 sont largement utilisés et ont peu de chances de troubler un programmeur
      Le vrai point de rupture, c’est quand deux programmes différents définissent chacun u16, l’exposent dans leurs fichiers d’en-tête, puis qu’un troisième programme inclut les deux en-têtes en même temps
      Les types de bibliothèque avec espace de noms finissent sous une forme comme libname_u32, et à ce stade on a plutôt envie d’utiliser simplement uint32_t au lieu du préfixe libname_
    • La possibilité qu’un programmeur C compétent soit désorienté en voyant u8 ou i32 est au mieux théorique, et ressemble un peu à un homme de paille
      Il peut trouver cela agaçant, mais il ne sera pas confus. Comme l’a dit Rich Hickey, tout est difficile à lire avant d’avoir appris à le lire
  • On dit qu’utiliser des booléens sur 32 bits peut sembler être un gaspillage de mémoire pour un débutant ; dans ce cas, je dois être débutant moi aussi
    J’ai entendu citer quelques cas où ce n’est pas pire qu’un bool sur 8 bits, mais je ne vois pas de cas où ce serait réellement meilleur. S’il y a des booléens adjacents dans une structure, ou si une variable booléenne d’une fonction est évincée des registres et placée sur la pile, cela gaspille toujours de la mémoire
    Même pour quelques octets, je ne comprends pas pourquoi on pessimiserait volontairement. Qu’est-ce qu’on gagne à utiliser une taille plus grande ?

    • Cela dépend entièrement de l’architecture et du CPU, mais d’après mon expérience passée, le cas évident était celui des traitements numériques
      Il y avait une valeur de condition au début d’une structure par cycle, suivie de 512, 1024 ou 2048 valeurs d’échantillons ; un junior, voulant économiser de l’espace, a compacté la structure et transformé la valeur de condition en un octet sur 8 bits
      Ce code « amélioré » a fait chuter le débit d’environ 10× sur des puces Intel, et provoquait un BUS ERROR sur l’architecture RISC SPARC
      En compactant l’en-tête de la structure, le tableau de données n’était plus aligné ; Intel devait silencieusement charger deux mots de 32 bits et les recombiner, tandis que SPARC se fâchait, comme il se doit, contre les données non alignées
      Pour un calcul en pipeline où le débit compte, et non pour du stockage de fichiers à long terme, il vaut parfois mieux aligner les données selon l’alignement de l’architecture plutôt que les compacter pour « économiser de l’espace »
    • Le plus souvent, l’optimisation facile consiste à ajouter du padding aux champs de la structure pour les aligner sur des frontières de 32 bits
      Presque tous les compilateurs le font, donc il suffit de chercher « alignement/padding de structure ». Si le compilateur va de toute façon laisser un espace vide, autant utiliser soi-même cette mémoire ; sinon, on risque de perdre en performances
      Plus précisément, chaque champ doit se trouver à une adresse divisible par sa propre taille ou par la taille d’une ligne de mot, et la structure entière doit aussi être complétée par du padding jusqu’à un multiple de la taille de son plus grand champ. En pratique, cela signifie généralement un alignement sur 32 bits
      Référence : http://www.catb.org/esr/structure-packing/
    • Si l’on utilise le vrai type bool, un sanitizer avertit lorsque la valeur n’est ni 0 pour false, ni 1 pour true
    • Les architectures informatiques sont, pour la plupart, optimisées pour des accès alignés sur 32 bits ou plus. Ce qu’on y gagne est généralement, mais pas toujours, de la performance
    • Je me demande à quoi ressemblerait une fonction où une variable booléenne est évincée sur la pile et où ces 3 octets deviennent importants
  • Je ne suis pas d’accord avec l’argument sur le retour de structures et les paramètres de sortie
    Cela rend beaucoup plus difficile la composition de fonctions pouvant retourner une erreur, et les types prolifèrent partout. En pratique, presque toutes les fonctions peuvent échouer ; donc, surtout si l’on prend aussi en compte les pénuries de mémoire, un style prévisible de retour d’erreurs est plus important

    • Presque personne ne gère les pénuries de mémoire
      C’est très difficile et le bénéfice est minime. À ce stade, les problèmes qui apparaissent sont d’une tout autre nature que le choix d’un style de programmation
    • Si un dépaquetage de structures significatif avait été possible, on aurait pu obtenir la sémantique habituelle de errno et des paramètres de sortie, mais le C n’en dispose pas
      Cela dit, composer des valeurs optionnelles en C a toujours été un peu pénible. Si l’on n’utilise pas d’exceptions, les deux grands choix selon les langages semblent être les exceptions et les monades, mais aucun des deux ne convient au C, ni à la philosophie de la plupart des programmeurs C
      Pour de simples appels un-à-un, on peut tenter des macros, mais elles ont leurs limites. Même si le C++ est épouvantable, C++ optional est plus agréable à utiliser que if(foo(x,y, out1, out2) != WHATEVER_LIBRARY_OK) { ... }
    • Retourner une option ou un type somme serait la bonne approche, mais en C c’est vraiment fastidieux à écrire
      Le motif consistant à ajouter if (thing(...)) goto fail à chaque appel de fonction ne paraît pas non plus très élégant, même si le camp Go semble l’apprécier
      Sinon, il y a thread_local mylibrary_errno, qui peut en fait être la bonne méthode à l’intérieur d’une bibliothèque, puis être converti en valeur de retour énumérée aux frontières
  • À partir de « signed sizes are the way », j’ai envie de dire qu’on pouvait s’arrêter là
    Une taille signée est une fuite d’abstraction très surprenante et une recette pour la catastrophe
    J’ai aussi du mal à accepter l’idée que const n’a pas de rôle pratique et n’a jamais permis de détecter d’erreur. Les gens confondent souvent les buffers d’entrée et de sortie, et const le révèle immédiatement
    Dire que toutes les fonctions, sauf les points d’entrée, doivent être static peut aussi faire qu’en débogage on ne retrouve plus une variable ou une fonction, au point de maudire l’auteur
    Préférer le retour de structures facilite les erreurs où l’on renvoie un pointeur de pile, ouvrant une grosse faille de sécurité. Passer un buffer de sortie rend la sémantique de propriété claire
    Ces conseils conviennent peut-être à peu près à quelqu’un qui écrit surtout du code système 64 bits, mais dans l’embarqué 32 bits, ils peuvent rapidement poser problème

    • Bjarne Stroustrup a écrit une note détaillée défendant les tailles signées
      https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p14...
    • Les gens qui détestent const ne seront jamais satisfaits ; il suffit donc d’utiliser const là où c’est nécessaire, de le propager autant qu’il le faut, puis d’ignorer les plaintes
      S’ils l’enlèvent, il faut le remettre. Ce sont toujours eux qui cèdent en premier. Ça fait 25 ans que je fais comme ça, et je suis toujours là
      static peut dépendre des outils. Il y a une quinzaine d’années, je suis passé au static par défaut et à l’usage généralisé de size_t, et je n’ai toujours pas rencontré de problème
  • Il est intéressant de voir que mon expérience m’a mené dans une autre direction
    https://dlang.org/blog/2023/10/02/crafting-self-evident-code...
    L’article est centré sur D, mais les principes s’appliquent aussi à C

    • Lecture intéressante
      La partie où les conditions sont déplacées à l’intérieur de doX() et doZ() m’a intrigué. Je ne sais pas si c’est toujours juste ; cela dépend de l’emplacement de l’abstraction et du modèle mental qu’on a du code
      Par exemple, deleteRecords(); n’est pas mieux que if let x = deadRecords() deleteRecords(x);. La seconde forme paraît plus brouillonne, mais elle a l’intérêt de montrer d’emblée qu’il s’agit d’élagage, pas de suppression
      Si l’on renomme intelligemment la fonction en pruneDeadProjects(), c’est correct, mais simplement déplacer la condition dans la fonction peut rendre le contexte dangereux et devenir une abstraction fuyante
  • Je suis favorable à l’usage de typedef pour toutes les structures, car cela aide à la concision
    Je pense qu’on peut utiliser typedef assez largement. En revanche, mieux vaut ne typedefer que le type lui-même, pas les pointeurs. Si l’on a besoin d’un pointeur, on peut toujours écrire (type *)
    En particulier, pour les pointeurs de fonction, il ne faut pas typedefer le pointeur de fonction, mais la fonction elle-même. Ainsi, on peut aussi utiliser ce typedef dans les déclarations de fonctions pour bénéficier de la vérification des types des paramètres, et il n’est pas nécessaire de corriger toutes les déclarations quand la signature change
    La plupart des bases de code C se trompent là-dessus : elles typedefent le pointeur de fonction et doivent quand même écrire manuellement les déclarations de fonctions correspondant à cette définition de pointeur
    Je ne suis toujours pas convaincu par l’usage de structures comme types de retour. Je préfère avoir un code d’erreur numérique comme valeur de retour, et recevoir les autres valeurs de retour via des paramètres de sortie

    • Pour les structures opaques, je préfère utiliser typedef pour imiter des classes dont tous les champs sont privés, et utiliser struct pour les structures de données simples
      On ne devrait accéder aux classes que par des fonctions, tandis que les structures devraient être directement accessibles
      C’est globalement plus proche des conventions standard de C/POSIX. C’est par exemple la différence entre pthread_t et struct stat
    • Je suis d’accord qu’il ne faut pas typedefer les pointeurs eux-mêmes
      Des bibliothèques comme SDL_net font exactement ça, et je n’aime pas. En réalité, c’est un pointeur, mais il est typedefé comme si c’était un type valeur
      Je comprends l’intention, mais c’est une approche assez gênante
  • Une grande partie de cet article me paraît convaincante
    J’ai commencé à écrire un OS bare metal pour Arm64 ; c’est encore au tout début, mais je fais des choses similaires. J’utilise des chaînes Pascal et j’ai aussi changé les noms de types. C’est simplement le style int8, pas i8
    Comme j’ai rapidement décidé que je n’avais pas l’intention de porter de vrais logiciels, je n’ai pas besoin de suivre les fonctions ni les conventions de la bibliothèque C standard. Cela me laisse davantage de liberté pour expérimenter
    C est un langage si ancien qu’il porte encore, jusque dans les noms de fonctions, le poids d’une époque où chaque octet était précieux. S’en libérer fait du bien, et le contenu de cet article, ainsi que plusieurs petits changements de noms, donnent l’impression d’un nettoyage assez élégant

    • Dire qu’il n’est pas nécessaire de suivre les fonctions ni les conventions de la bibliothèque C standard, cela veut-il dire que c’est juste un projet de loisir et que ça ne deviendra pas quelque chose de gros et professionnel comme GNU ?
    • Je n’avais jamais pensé au fait d’économiser des octets même dans les symboles
  • typedef float f32;, typedef double f64; ressemblent à un raccourci dangereux qui suppose que float fait 32 bits et que double fait 64 bits
    OpenCV définit float16_t, CUDA implémente les flottants en demi-précision, et les microcontrôleurs peuvent chacun avoir leur propre implémentation
    C++23 introduit des types à virgule flottante de largeur fixe, mais je ne sais pas comment l’imposer en C. Il me semblerait préférable d’avoir une macro qui vérifie à la compilation qu’il n’y a pas de perte de données
    Globalement, comme d’autres l’ont dit, même si c’est moins concis, il peut être préférable d’en laisser une partie telle quelle par défaut pour préserver la lisibilité
    [0] https://docs.opencv.org/4.x/df/dc9/classcv_1_1float16__t.htm...
    [1] https://docs.nvidia.com/cuda/cuda-math-api/group__CUDA__MATH...
    [2] https://en.cppreference.com/w/cpp/types/floating-point