Funktioner som returnerar strängar, bra stil?

I mina C-program behöver jag ofta ett sätt att göra en strängrepresentation av mina ADT. Även om jag inte behöver skriva ut strängen på skärmen på något sätt är det snyggt att ha en sådan metod för felsökning. Så den här typen av funktion kommer ofta upp.

char * mytype_to_string( const mytype_t *t ); 

Jag inser faktiskt att jag har (åtminstone) tre alternativ här för att hantera minnet för att strängen ska returneras.

Alternativ 1: Lagring av retursträngen i en statisk char-array i funktionen . Jag behöver inte tänka mycket, förutom att strängen skrivs över vid varje samtal. Vilket kan vara ett problem vid vissa tillfällen.

Alternativ 2: Tilldela strängen på högen med malloc inuti funktionen. Riktigt snyggt eftersom jag då inte behöver tänka på storleken på en buffert eller överskrivningen. Men jag måste komma ihåg att frigöra () strängen när jag är klar, och sedan måste jag också tilldela en tillfällig variabel så att Jag kan frigöra. Och då är högtilldelning verkligen mycket långsammare än stackallokering, var därför en flaskhals om detta upprepas i en slinga.

Alternativ 3: Skicka pekaren till en buffert och låt den som ringer allokera den bufferten. Som:

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

Detta ger uppringaren större ansträngning. Jag märker också att detta alternativ ger mig ett annat alternativ i ordningens argument. Vilket argument ska jag ha först och sist? (Faktiskt sex möjligheter)

Så, vad ska jag föredra? Något varför? Finns det någon form av oskriven standard bland C-utvecklare?

Kommentarer

  • Bara en observationsnot, de flesta operativsystem använder alternativ 3 – den som ringer tilldelar buffert på något sätt; berättar buffertpekaren och kapaciteten, callee fyller buff er och returnerar också faktisk stränglängd om bufferten är otillräcklig. Exempel: sysctlbyname i OS X och iOS

Svar

Metoderna jag sett mest är 2 och 3.

Den användarbaserade bufferten är faktiskt ganska enkel att använda:

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

Även om de flesta implementeringar returnerar den använda buffertmängden.

Alternativ 2 blir långsammare och är farligt när dynamiskt länkade bibliotek används där de kan använda olika körtider (och olika högar). Så du kan inte frigöra det som har malocerats i ett annat bibliotek. Detta kräver då en free_string(char*) -funktion för att hantera den.

Kommentarer

  • Tack! Jag tycker också att jag gillar alternativ 3 bäst. Men jag vill kunna göra saker som: printf("MyType: %s\n", mytype_to_string( mt, buf, sizeof(buf)); och därmed vann jag ’ inte att returnera den använda längden utan snarare pekaren till strängen. Den dynamiska bibliotekskommentaren är väldigt viktig.
  • Bör ’ t detta ska vara sizeof(buffer) - 1 för att tillfredsställa \0 terminator?
  • @ Michael-O nej null-termen ingår i buffertstorleken vilket innebär att maxsträngen som kan sättas in är 1 mindre än den passerade storleken. Detta är mönstret som den säkra strängen fungerar i standardbiblioteket som snprintf.
  • @ratchetfreak Tack för förtydligandet. Skulle vara trevligt att utöka svaret med den visdomen.

Svar

Ytterligare designidé för # 3

Ange också den maximala storlek som behövs för mytype i samma .h-fil som mytype_to_string().

#define MYTYPE_TO_STRING_SIZE 256 

Nu kan användaren koda därefter.

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

Beställ

Storleken på matriser, när det är första, tillåter VLA-typer.

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

Inte så viktigt med enstaka dimension, men ändå användbart med 2 eller fler.

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

Jag minns att läsa att ha storleken först är ett föredraget idiom i nästa C. Behöver du hitta den referensen ….

Svar

Som ett tillägg till @ratchetfreaks utmärkta svar vill jag påpeka att alternativ # 3 följer ett liknande paradigm / mönster som standard C-biblioteksfunktioner.

Till exempel strncpy.

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

Att följa samma paradigm skulle hjälpa för att minska den kognitiva belastningen för nya utvecklare (eller till och med ditt framtida jag) när de behöver använda din funktion.

Den enda skillnaden med vad du har i ditt inlägg skulle vara att i C-biblioteken tenderar att listas först i argumentlistan.Så:

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

Svar

I ”echo @ratchet_freak i de flesta fall (kanske med en liten tweak för sizeof buffer över 128) men jag vill hoppa in här med ett konstigt svar. Vad sägs om att vara konstig? Varför inte, förutom problemen med att få konstiga blickar från våra kollegor och behöva vara extra övertygande? Och jag erbjuder detta:

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

Och exempel på användning:

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

Om du gjorde det på det här sättet kan du göra din fördelare mycket effektiv (effektivare) än malloc både när det gäller allokering / deallokationskostnader och även förbättrad referensplats för minnesåtkomst. och poolminne på ett sekventiellt sätt från stora angränsande block (med det första blocket som inte ens kräver en hög al plats – det kan tilldelas på stacken). Det förenklar felhantering. Och detta kan vara det mest diskutabla men jag tycker att det är praktiskt taget uppenbart när det gäller hur det gör det tydligt för den som ringer att det kommer att allokera minne till den allokerare du skickar in, vilket kräver uttrycklig frigöring (allocator_purge i detta fall) utan att behöva dokumentera sådant beteende i varje enskild funktion om du använder den här stilen konsekvent. Godkännandet av allokeringsparametern gör det förhoppningsvis ganska uppenbart.

Jag vet inte. Jag får motargument här som att implementera den enklast möjliga arenatilldelaren som är möjlig (använd bara maximal anpassning för alla förfrågningar) och att hantera det är för mycket arbete. Min trubbiga tanke är som, vad är vi, Python-programmerare? Kan lika gärna använda Python om så är fallet. Använd Python om dessa detaljer inte spelar någon roll. Jag är seriös. Jag har haft många C-programmeringskollegor som med stor sannolikhet skulle skriva inte bara mer korrekt kod utan möjligen ännu effektivare med Python eftersom de ignorerar saker som referensplats medan de snubblar över buggar som de skapar åt vänster och höger. Jag gör det inte. t se vad det är som är så läskigt med någon enkel arenatilldelare här om vi är C-programmerare som är intresserade av saker som datalokalitet och optimalt instruktionsval, och detta är utan tvekan mycket mindre att tänka på än åtminstone de typer av gränssnitt som kräver uppringare att uttryckligen frigöra varje enskild sak som gränssnittet kan returnera. Det erbjuder bulkdeallocation över individuell deallocation av ett slag som är mer felbenägen. En riktig C-programmerare utmanar loopy-samtal till malloc som jag ser det, särskilt när det visas som en hotspot i deras profil. Från min synpunkt måste det finnas mer ” oomph ” till en motivering för att fortfarande vara C-programmerare 2020, och vi kan ” t skjuter bort saker som minnesallokatorer längre.

Och detta har inte kantfallet att tilldela en buffert med fast storlek där vi tilldelar, säg 256 byte och den resulterande strängen är större. Även om våra funktioner undviker buffertöverskridanden (som med sprintf_s), finns det mer kod för att korrekt återhämta sig från sådana fel som krävs som vi kan utelämna med allokeringsfallet eftersom det inte ”t har dessa kantfall. Vi behöver inte hantera sådana fall i allokeringsfallet, såvida vi inte verkligen tömmer det fysiska adresseringsutrymmet för vår hårdvara (som ovanstående kod hanterar, men det behöver inte hantera ” från förut tilldelad buffert ” separat från ” utan minne ”).

Svar

Förutom att det du föreslår är en dålig kodlukt, alternativ 3 låter bäst för mig. Jag tycker också som @ gnasher729 att du använder fel språk.

Kommentarer

Svar

För att vara ärlig kanske du vill byta till ett annat språk där retur av en sträng inte är en komplicerad, arbetsintensiv och felbenägen operation.

Du kan överväga C ++ eller Objective-C där du kan lämna 99% av din kod oförändrad.

Lämna ett svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *