Funcții care returnează șiruri, stil bun?

În programele mele C am adesea nevoie de o modalitate de a face o reprezentare în șir a ADT-urilor mele. Chiar dacă nu trebuie să tipăresc șirul pe ecran în vreun fel, este bine să ai o astfel de metodă pentru depanare. Deci, acest tip de funcție apare adesea.

char * mytype_to_string( const mytype_t *t ); 

De fapt, îmi dau seama că am (cel puțin) trei opțiuni aici pentru gestionarea memoriei pentru restituirea șirului.

Alternativa 1: Stocarea șirului de întoarcere într-o matrice de caractere statică în funcție Nu am nevoie de multă gândire, cu excepția faptului că șirul este suprascris la fiecare apel. Ceea ce poate fi o problemă în unele ocazii.

Alternativa 2: Alocați șirul de pe heap cu malloc în interiorul funcției. Într-adevăr îngrijit, deoarece nu am câștigat atunci, nu trebuie să mă gândesc la dimensiunea unui tampon sau la suprascriere. Cu toate acestea, trebuie să-mi amintesc să eliberez () șirul atunci când ați terminat, și apoi trebuie să aloc o variabilă temporară astfel încât Pot elibera. Și atunci alocarea heap este într-adevăr mult mai lentă decât alocarea stivei, prin urmare fii un blocaj dacă acest lucru se repetă într-o buclă.

Alternativa 3: Treci în pointer într-un buffer și lasă apelantul să aloce acel tampon. Ca:

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

Acest lucru aduce mai mult efort apelantului. De asemenea, observ că această alternativă îmi oferă o altă opțiune în ordinea argumentelor. Ce argument ar trebui să am primul și ultimul? (De fapt șase posibilități)

Deci, care ar trebui să prefer? Orice de ce? Există un fel de standard nescris printre dezvoltatorii C?

Comentarii

  • Doar o notă de observație, majoritatea sistemelor de operare folosesc opțiunea 3 – apelantul alocă tampon oricum; spune indicatorul și capacitatea bufferului; er și, de asemenea, returnează lungimea reală a șirului dacă bufferul este insuficient. Exemplu: sysctlbyname în OS X și iOS

Răspuns

Metodele pe care le-am văzut cel mai mult sunt 2 și 3.

Tamponul furnizat de utilizator este de fapt destul de simplu de utilizat:

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

Deși majoritatea implementărilor vor returna cantitatea de tampon utilizată.

Opțiunea 2 va fi mai lentă și este periculoasă atunci când se utilizează biblioteci conectate dinamic unde pot utiliza runtimes diferite (și diferite grămezi). Deci, nu puteți elibera ceea ce a fost mallocat într-o altă bibliotecă. Acest lucru necesită apoi o funcție free_string(char*) pentru a face față acesteia.

Comentarii

  • Mulțumesc! Cred că îmi place și cel mai bine Alternativa 3. Cu toate acestea, vreau să pot face lucruri precum: printf("MyType: %s\n", mytype_to_string( mt, buf, sizeof(buf)); și, prin urmare, mi-a plăcut ‘ să returnez lungimea utilizată, ci mai degrabă pointerul la coardă. Comentariul dinamic al bibliotecii este foarte important.
  • Nu ar trebui ca ‘ să fie sizeof(buffer) - 1 pentru a satisface \0 terminator?
  • @ Michael-O nu, termenul nul este inclus în dimensiunea bufferului, ceea ce înseamnă că șirul maxim care poate fi introdus este cu 1 mai mic decât dimensiunea trecută. Acesta este modelul pe care șirul sigur îl funcționează în biblioteca standard, cum ar fi snprintf, pe care îl utilizați.
  • @ratchetfreak Vă mulțumim pentru clarificare. Ar fi bine să extindem răspunsul cu această înțelepciune.

Răspuns

Ideea de proiectare suplimentară pentru # 3

Când este posibil, furnizați și dimensiunea maximă necesară pentru mytype în același fișier .h ca mytype_to_string().

#define MYTYPE_TO_STRING_SIZE 256 

Acum utilizatorul poate codifica în consecință.

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

Comandă

Dimensiunea matricilor, când este prima, permite tipuri de VLA.

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

Nu este atât de important cu o singură dimensiune, dar util cu 2 sau mai multe.

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

Îmi amintesc că citirea având dimensiunea mai întâi este un idiom preferat în C. următoare. Trebuie să găsesc acea referință ….

Răspuns

În plus față de răspunsul excelent al lui @ratchetfreak, aș sublinia că alternativa # 3 urmează o paradigmă / model similar cu funcțiile standard ale bibliotecii C.

De exemplu, strncpy.

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

Respectarea aceleiași paradigme ar ajuta pentru a reduce încărcătura cognitivă pentru noii dezvoltatori (sau chiar pentru viitorul dvs. sine) atunci când aceștia trebuie să vă utilizeze funcția.

Singura diferență cu ceea ce aveți în postarea dvs. ar fi că din bibliotecile C tinde să fie listat mai întâi în lista de argumente.Deci:

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

Răspuns

I „d echo @ratchet_freak în majoritatea cazurilor (poate cu o mică modificare pentru sizeof buffer peste 128) dar vreau să intru aici cu un răspuns ciudat. Ce zici de a fi ciudat? De ce nu, pe lângă problemele legate de obținerea unor priviri ciudate de la colegii noștri și de a fi mai convingători? Și ofer acest lucru:

// 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; } 

Și exemplu de utilizare:

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); } 

Dacă ați făcut acest lucru, puteți face alocatorul dvs. foarte eficient (mai eficient decât malloc atât în ceea ce privește costurile de alocare / repartizare, cât și localitatea de referință îmbunătățită pentru accesul la memorie). Poate fi un alocator de arenă care implică doar incrementarea unui indicator în cazurile obișnuite pentru solicitări de alocare și memorie pool într-un mod secvențial din blocuri adiacente mari (cu primul bloc nici măcar nu necesită un heap al locație – poate fi alocat pe stivă). Simplifică gestionarea erorilor. De asemenea, și acest lucru ar putea fi cel mai discutabil, dar cred că este practic destul de evident în ceea ce privește modul în care clarifică apelantului că va aloca memorie alocatorului pe care îl treceți, necesitând eliberarea explicită (allocator_purge în acest caz) fără a fi nevoie să documentați un astfel de comportament în fiecare funcție posibilă dacă utilizați acest stil în mod consecvent. Acceptarea parametrului alocator îl face să sperăm destul de evident.

Nu știu. Am aici contraargumente, cum ar fi implementarea celui mai simplu posibil alocator de arenă posibil (folosiți doar alinierea maximă pentru toate cererile) și Gestionarea mea este prea mare. Gândul meu direct este de ce suntem noi, programatorii Python? S-ar putea folosi Python la fel ca da. Vă rugăm să folosiți Python dacă aceste detalii nu contează. Sunt „serios. Am avut mulți colegi de programare C care ar scrie foarte probabil nu numai coduri mai corecte, ci și mai eficiente cu Python, deoarece ignoră lucruri precum localitatea de referință, în timp ce se poticnesc cu bug-urile pe care le creează în stânga și în dreapta. nu văd ce este atât de înfricoșător în cazul unui simplu alocator de arenă aici dacă suntem programatori C preocupați de lucruri precum localitatea datelor și selecția optimă a instrucțiunilor, iar acest lucru este, fără îndoială, mult mai puțin de gândit decât cel puțin tipurile de interfețe care necesită apelanții să elibereze în mod explicit fiecare lucru individual pe care interfața îl poate returna. Oferă repartizare în bloc peste repartizare individuală de un fel care este mai predispus la erori. Un programator C adecvat provoacă apelurile loopy către malloc așa cum îl văd, mai ales atunci când apare ca un hotspot în profilerul lor. Din punctul meu de vedere, trebuie să existe mai mult ” oomph ” la o justificare pentru a fi încă programator C în 2020 și putem ” Nu te mai feri de lucruri precum alocatoarele de memorie.

Și acest lucru nu are cazurile marginale ale alocării unui buffer de dimensiuni fixe în care alocăm, să zicem, 256 de octeți, iar șirul rezultat este mai mare. Chiar dacă funcțiile noastre evită depășirile de tampon (cum ar fi cu sprintf_s), există mai multe coduri pentru a recupera corect din astfel de erori care sunt necesare, pe care le putem omite cu cazul alocatorului, deoarece nu „Nu avem aceste cazuri marginale. Nu trebuie să ne ocupăm de astfel de cazuri în cazul alocatorului, cu excepția cazului în care epuizăm cu adevărat spațiul de adresare fizică al hardware-ului nostru (pe care îl gestionează codul de mai sus, dar nu trebuie să se ocupe de ” din memoria tampon prealocată ” separat de ” din memorie „).

Răspuns

Pe lângă faptul că propuneți să faceți este un miros de cod prost, alternativa 3 mi se pare cel mai bine. De asemenea, cred ca @ gnasher729 că folosești un limbaj greșit.

Comentarii

Răspuns

Pentru a fi sincer, este posibil să doriți să treceți la o altă limbă în care returnarea unui șir nu este o operație complexă, intensivă de lucru și predispusă la erori.

Ați putea lua în considerare C ++ sau Objective-C unde puteți lăsa 99% din codul dvs. neschimbat.

Lasă un răspuns

Adresa ta de email nu va fi publicată. Câmpurile obligatorii sunt marcate cu *