- 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 deconstet destruct, 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
s8avecdataetlen; dans les environnements Win32 et UTF-16,c16ets16sont 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
okqu’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
- Exemples :
- 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
_tn’est plus utilisé ; il est désormais perçu comme un élément visuellement distrayant - Pour le préfixe des types signés,
iest préféré àssest réservé aux noms de types de chaînes
- Pour le type de taille,
sizeest utilisé plutôt queisize- Parce que la taille signée est considérée comme la valeur par défaut la plus importante
usizea un usage plus restreint, principalement lors des interactions avec des interfaces externes
b32exprime 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
- Une taille de mot naturelle est utilisée plutôt que
c16est 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
- S’il est basé sur
u8est utilisé pour les octets et principalement les données UTF-8, tandis quebyteest 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_tsont également considérés comme un gaspillage inutile
- Les noms de types longs comme
- Lorsqu’un extrait de code est montré isolément, ces alias ne sont pas utilisés seuls
- Car le lecteur a aussi besoin du
typedefpour comprendre le contexte
- Car le lecteur a aussi besoin du
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)
- Exemples :
ALL_CAPSreste 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 champnew - Car la macro ne se développe pas si elle n’a pas la forme d’un appel de fonction
- On peut avoir à la fois une macro
- La macro
assertpour GCC et Clang utilise la formewhile (!(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
libubsanfournit 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-trapet activer au minimum-fsanitize=unreachable -funreachable-trapspourrait 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
constn’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
constest un changement qui augmente la productivité en réduisant la charge cognitive et le bruit visuel - Petite exception :
constreste préféré comme indice pour placer les tables statiques en mémoire en lecture seule, près du code- Si nécessaire,
constest retiré par cast
- Si nécessaire,
- Pour les pointeurs nuls, le littéral
0est 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
restrictn’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
inlinen’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é
structrend 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
- Supprimer le mot-clé
- 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
constet destruct, 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
s8avecdataetlen - Structure de
s8:u8 *datasize len
- La macro
s8(s)enveloppe un littéral de chaîne C dans une chaînes8 s8se transmet et se retourne par valeur, comme un fat pointers8fonctionne aussi bien comme préfixe de fonction- Car les noms de la famille
strsont réservés - Exemples :
s8span,s8equals,s8compare,s8hash,s8trim,s8clone
- Car les noms de la famille
- 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é
s16est aussi utilisé comme type compatible UTF-16- Il contient
c16 *dataetsize len - L’ajout de
uaux littéraux dans la macro n’est pas encore considéré comme pleinement satisfaisant
- Il contient
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 foisvalue, le résultat du parsing, etok, 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
restrictcaché - Soit, une fois inliné, le surcoût de retour n’a plus d’importance
- Soit la convention d’appel le transforme en paramètre de sortie
- 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,
okest 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
__attributeest préféré à__attribute__- Le suffixe
__final est jugé excessif et inutile
- Le suffixe
- En programmation système Win32,
windows.hn’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,b32etuptr, plutôt qu’avecDWORD,BOOLouULONG_PTR
- Les exemples de déclarations Win32 utilisent la macro
W32(r) __declspec(dllimport) r __stdcall- Des fonctions comme
ExitProcess,GetStdHandle,VirtualAlloc,WriteConsoleAetWriteConsoleWsont déclarées directement
- Des fonctions comme
- 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
- Comme avec
- 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
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) * 3fonctionne 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 autoriseinteger[pointer]avec le même sens quepointer[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
countest 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.
_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_ALLOCATEDest défini, ce n’est pas forcément 1 mais n’importe quelle valeur non nulle ;_Boolla normalise à 1. Si on la stocke dans unint,if (need_free)passe, maisif (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
_Boolfaisait quelque chose d’utile, on peut aboutir à du code incorrect commeif ((flags & FLAG_ALLOCATED) == true).De plus, lorsqu’on lit une structure depuis le disque ou qu’on remplit des octets arbitraires, si un champ
_Booln’est ni 0 ni 1, il y a un risque de comportement indéfini.(size)(sizeof(x)[y])surprendra aussi beaucoup de gens, mais c’est équivalent à(size)(sizeof ((x)[y])).sizeofn’est pas une fonction, mais un opérateur unaire, et l’indexation comme les appels de fonction ont une priorité plus élevée quesizeof. C’est pourquoi je préfère mettre une espace aprèssizeofet 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))).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_tplutôt queuint.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.
intde 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 longfait toujours 64 bits, donc il suffit d’abandonnerlong.char8 bits,short16 bits,int32 bits,long long64 bits, et c’est réglé. En C, on a gaspillé un temps infini sur la taille deint.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.
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
typedefpour 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.
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
u8oui32au lieu deuint8_touint32_téconomise bien quelques caractères, mais peut semer la confusion quand quelqu’un d’autre lit le codeUtiliser 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.hpeut 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 tousu8oui32ne servent pas à économiser des frappes au clavier, mais à réduire la charge perceptive à la lectureL’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
u16sont largement utilisés et ont peu de chances de troubler un programmeurLe 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 tempsLes 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 simplementuint32_tau lieu du préfixelibname_u8oui32est au mieux théorique, et ressemble un peu à un homme de pailleIl 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
boolsur 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émoireMême pour quelques octets, je ne comprends pas pourquoi on pessimiserait volontairement. Qu’est-ce qu’on gagne à utiliser une taille plus grande ?
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 »
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/
bool, un sanitizer avertit lorsque la valeur n’est ni 0 pourfalse, ni 1 pourtrueJe 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
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
errnoet des paramètres de sortie, mais le C n’en dispose pasCela 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) { ... }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écierSinon, 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
constn’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, etconstle révèle immédiatementDire que toutes les fonctions, sauf les points d’entrée, doivent être
staticpeut aussi faire qu’en débogage on ne retrouve plus une variable ou une fonction, au point de maudire l’auteurPré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
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p14...
constne seront jamais satisfaits ; il suffit donc d’utiliserconstlà où c’est nécessaire, de le propager autant qu’il le faut, puis d’ignorer les plaintesS’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à
staticpeut dépendre des outils. Il y a une quinzaine d’années, je suis passé austaticpar défaut et à l’usage généralisé desize_t, et je n’ai toujours pas rencontré de problèmeIl 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
La partie où les conditions sont déplacées à l’intérieur de
doX()etdoZ()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 codePar exemple,
deleteRecords();n’est pas mieux queif 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 suppressionSi 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 fuyanteJe suis favorable à l’usage de
typedefpour toutes les structures, car cela aide à la concisionJe pense qu’on peut utiliser
typedefassez largement. En revanche, mieux vaut netypedefer 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 changeLa 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 pointeurJe 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
typedefpour imiter des classes dont tous les champs sont privés, et utiliserstructpour les structures de données simplesOn 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_tetstruct stattypedefer les pointeurs eux-mêmesDes 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 valeurJe 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, pasi8Comme 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
typedef float f32;,typedef double f64;ressemblent à un raccourci dangereux qui suppose quefloatfait 32 bits et quedoublefait 64 bitsOpenCV définit
float16_t, CUDA implémente les flottants en demi-précision, et les microcontrôleurs peuvent chacun avoir leur propre implémentationC++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
_Floattypedef _Float32 f32;typedef _Float64 f64;https://gcc.gnu.org/onlinedocs/gcc/Floating-Types.html