Fonctions renvoyant des chaînes, bon style?

Dans mes programmes C, jai souvent besoin dun moyen de faire une représentation sous forme de chaîne de mes ADT. Même si je n’ai pas besoin d’imprimer la chaîne à l’écran de quelque manière que ce soit, il est intéressant d’avoir une telle méthode pour le débogage. Donc, ce type de fonction apparaît souvent.

char * mytype_to_string( const mytype_t *t ); 

Je me rends compte en fait que jai ici (au moins) trois options pour gérer la mémoire de la chaîne à renvoyer.

Alternative 1: Stocker la chaîne de retour dans un tableau de caractères statiques dans la fonction Je nai pas besoin de beaucoup de réflexion, sauf que la chaîne est écrasée à chaque appel. Ce qui peut être un problème dans certaines occasions.

Alternative 2: Allouez la chaîne sur le tas avec malloc à lintérieur de la fonction. Vraiment chouette car je nai plus besoin de penser à la taille dun tampon ou à lécrasement. Cependant, je dois me rappeler de libérer () la chaîne une fois terminé, et ensuite je dois également attribuer une variable temporaire telle que Je peux libérer. Et puis lallocation de tas est vraiment beaucoup plus lente que lallocation de pile, donc être un goulot détranglement si cela se répète dans une boucle.

Alternative 3: Passer le pointeur vers un tampon, et laisser lappelant allouer ce tampon. Comme:

char * mytype_to_string( const mytype_t *mt, char *buf, size_t buflen ); 

Cela apporte plus defforts à lappelant. Je remarque également que cette alternative me donne une autre option sur lordre des arguments. Quel argument dois-je avoir en premier et en dernier? (En fait six possibilités)

Alors, lequel devrais-je préférer? Pourquoi? Existe-t-il une sorte de norme non écrite parmi les développeurs C?

Commentaires

  • Juste une note dobservation, la plupart des systèmes dexploitation utilisent loption 3 – lappelant alloue le tampon de toute façon; indique le pointeur et la capacité du tampon; lappelé remplit buff er et renvoie également la longueur réelle de la chaîne si le tampon est insuffisant. Exemple: sysctlbyname sous OS X et iOS

Réponse

Les méthodes que jai le plus vues sont 2 et 3.

Le tampon fourni par lutilisateur est en fait assez simple à utiliser:

char[128] buffer; mytype_to_string(mt, buffer, 128); 

Bien que la plupart des implémentations renvoient la quantité de tampon utilisée.

Loption 2 sera plus lente et dangereuse lors de lutilisation de bibliothèques liées dynamiquement où elles peuvent utiliser différents runtimes (et différents tas). Ainsi, vous ne pouvez pas libérer ce qui a été malléé dans une autre bibliothèque. Cela nécessite alors une fonction free_string(char*) pour le gérer.

Commentaires

  • Merci! Je pense aussi que jaime mieux lAlternative 3. Cependant, je veux pouvoir faire des choses comme: printf("MyType: %s\n", mytype_to_string( mt, buf, sizeof(buf)); et donc jai gagné ‘ t aimer renvoyer la longueur utilisée mais plutôt le pointeur à la chaîne. Le commentaire de la bibliothèque dynamique est vraiment important.
  • Ne devrait ‘ que ce soit sizeof(buffer) - 1 pour satisfaire le \0 terminator?
  • @ Michael-O non le terme nul est inclus dans la taille de la mémoire tampon, ce qui signifie que la chaîne maximale qui peut être insérée est inférieure de 1 à la taille transmise. Cest le modèle que la chaîne de sécurité fonctionne dans la bibliothèque standard comme snprintf utilise.
  • @ratchetfreak Merci pour la clarification. Ce serait bien détendre la réponse avec cette sagesse.

Réponse

Idée de conception supplémentaire pour # 3

Dans la mesure du possible, indiquez également la taille maximale nécessaire pour mytype dans le même fichier .h que mytype_to_string().

#define MYTYPE_TO_STRING_SIZE 256 

Maintenant, lutilisateur peut coder en conséquence.

char buf[MYTYPE_TO_STRING_SIZE]; puts(mytype_to_string(mt, buf, sizeof buf)); 

Ordre

La taille des tableaux, lorsquelle est la première, autorise les types VLA.

char * mytype_to_string( const mytype_t *mt, size_t bufsize, char *buf[bufsize]); 

Pas si important avec une seule dimension, mais utile avec 2 ou plus.

void matrix(size_t row, size_t col, double matrix[row][col]); 

Je me souviens avoir lu avoir la taille en premier est un idiome préféré dans le prochain C. Besoin de trouver cette référence ….

Réponse

En complément de lexcellente réponse de @ratchetfreak, je tiens à souligner que lalternative n ° 3 suit un paradigme / modèle similaire à celui des fonctions standard de la bibliothèque C.

Par exemple, strncpy.

 char * strncpy ( char * destination, const char * source, size_t num );  

Suivre le même paradigme aiderait pour réduire la charge cognitive des nouveaux développeurs (ou même de votre futur moi) lorsquils auront besoin dutiliser votre fonction.

La seule différence avec ce que vous avez dans votre message serait que le dans les bibliothèques C a tendance à être répertorié en premier dans la liste darguments.Donc:

 char * mytype_to_string( char *buf, const mytype_t *mt, size_t buflen );  

Réponse

Je « d echo @ratchet_freak dans la plupart des cas (peut-être avec un petit ajustement pour sizeof buffer sur 128) mais je veux sauter ici avec une réponse bizarre. Que diriez-vous dêtre bizarre? Pourquoi pas, en plus des problèmes dobtenir des regards étranges de nos collègues et de devoir être plus persuasif? Et je propose ceci:

// Note allocator parameter. char* mytype_to_string(allocator* alloc, const mytype_t* t) { char* buf = allocate(alloc, however_much_you_need); // fill out buf based on "t" contents return buf; } 

Et exemple dutilisation:

void func(my_type a, my_type b) { allocator alloc = allocator_new(); const char* str1 = mytype_to_string(&alloc, &a); if (!str1) goto oom; const char* str2 = mytype_to_string(&alloc, &b); if (!str2) goto oom // do something with str1 and str2 goto finish; oom: errno = ENOMEM; finish: // Frees all memory allocated through `alloc`. allocator_purge(&alloc); } 

Si vous lavez fait de cette façon, vous pouvez rendre votre allocateur très efficace (plus efficace que malloc à la fois en termes de coûts dallocation / de désallocation et également amélioration de la localité de référence pour laccès à la mémoire. et la mémoire de pool de manière séquentielle à partir de grands blocs contigus (avec le premier bloc ne nécessitant même pas un tas al emplacement – il peut être alloué sur la pile). Cela simplifie la gestion des erreurs. Aussi, et cest peut-être le plus discutable, mais je pense que cest pratiquement assez évident en termes de comment il indique clairement à lappelant quil va allouer de la mémoire à lallocateur que vous passez, ce qui nécessite une libération explicite (allocator_purge dans ce cas) sans avoir à documenter un tel comportement dans chaque fonction possible si vous utilisez ce style de manière cohérente. Lacceptation du paramètre dallocateur le rend, espérons-le, assez évident.

Je ne sais pas. Jobtiens ici des contre-arguments comme limplémentation de lallocateur darène le plus simple possible (il suffit dutiliser lalignement maximum pour toutes les requêtes) et faire face est trop de travail. Ma pensée directe est la suivante: que sommes-nous, les programmeurs Python? Autant utiliser Python si oui. Veuillez utiliser Python si ces détails nont pas dimportance. Je suis sérieux. Jai eu de nombreux collègues de programmation C qui écriraient très probablement non seulement du code plus correct, mais peut-être même plus efficace avec Python car ils ignorent des choses comme la localité de référence tout en trébuchant sur les bogues quils créent à gauche et à droite. Je ne le fais pas. t voir ce quil y a de si effrayant à propos dun simple allocateur darène ici si nous sommes des programmeurs C concernés par des choses comme la localisation des données et la sélection optimale des instructions, et cest sans doute beaucoup moins à penser quau moins les types dinterfaces qui nécessitent appelants pour libérer explicitement chaque élément individuel que linterface peut renvoyer. Il offre une désallocation en masse sur une désallocation individuelle dune sorte qui est plus sujette aux erreurs. Un bon programmeur C défie les appels en boucle à malloc comme je le vois, surtout quand il apparaît comme un hotspot dans leur profileur. De mon point de vue, il doit y avoir plus de  » oomph  » pour justifier dêtre toujours programmeur C en 2020, et nous pouvons  » t évitez plus des choses comme les allocateurs de mémoire.

Et cela na pas les cas extrêmes dallouer un tampon de taille fixe où nous allouons, disons, 256 octets et la chaîne résultante est plus grande. Même si nos fonctions évitent les dépassements de tampon (comme avec sprintf_s), il y a plus de code pour récupérer correctement de telles erreurs qui sont nécessaires que nous pouvons omettre avec le cas dallocateur car il ne le fait pas « Nous navons pas ces cas extrêmes. Nous navons pas à traiter de tels cas dans le cas de lallocateur, à moins que nous népuisions vraiment lespace dadressage physique de notre matériel (que le code ci-dessus gère, mais il na pas à traiter  » hors tampon pré-alloué  » séparément de  » mémoire insuffisante « ).

Réponse

Outre le fait que ce que vous proposez de faire est un mauvaise odeur de code, lalternative 3 me convient le mieux. Je pense aussi, comme @ gnasher729, que vous utilisez le mauvais langage.

Commentaires

Réponse

Pour être honnête, vous voudrez peut-être passer à une autre langue où le renvoi dune chaîne nest pas une opération complexe, intensive en travail et sujette aux erreurs.

Vous pourriez envisager C ++ ou Objective-C où vous pourriez laisser 99% de votre code inchangé.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *