Funktioner, der returnerer strenge, god stil?

I mine C-programmer har jeg ofte brug for en måde at lave en streng repræsentation af mine ADTer. Selvom jeg ikke behøver at udskrive strengen til skærmen på nogen måde, er det pænt at have en sådan metode til fejlfinding. Så denne form for funktion kommer ofte op.

char * mytype_to_string( const mytype_t *t ); 

Jeg er faktisk klar over, at jeg her (i det mindste) har tre muligheder for at håndtere hukommelsen, som strengen skal returnere.

Alternativ 1: Lagring af returstrengen i en statisk char-array i funktionen … Jeg har ikke brug for meget overvejelser, bortset fra at strengen overskrives ved hvert opkald. Hvilket kan være et problem i nogle tilfælde.

Alternativ 2: Tildel strengen på bunken med malloc inde i funktionen. Virkelig pænt, da jeg da ikke behøver at tænke på størrelsen på en buffer eller overskrivningen. Dog er jeg nødt til at huske at frigøre () strengen, når det er gjort, og så skal jeg også tildele en midlertidig variabel, så Jeg kan frigøre. Og så er bunktildeling virkelig meget langsommere end stakallokering, vær derfor en flaskehals, hvis dette gentages i en løkke.

Alternativ 3: Send markør til en buffer, og lad den, der ringer op, allokere som bufferen. Som:

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

Dette bringer mere indsats for den, der ringer op. Jeg bemærker også, at dette alternativ giver mig en anden mulighed i rækkefølgen af argumenterne. Hvilket argument skal jeg have første og sidste? (Faktisk seks muligheder)

Så hvad skal jeg foretrække? Enhver hvorfor? Er der en slags uskrevet standard blandt C-udviklere?

Kommentarer

  • Bare en observationsnote, de fleste operativsystemer bruger mulighed 3 – opkald tildeler alligevel buffer; fortæller bufferpointer og kapacitet; callee fylder buff er og returnerer også den aktuelle længde på strengen hvis bufferen er utilstrækkelig. Eksempel: sysctlbyname i OS X og iOS

Svar

De metoder, jeg har set mest, er 2 og 3.

Den brugerforsynede buffer er faktisk ret enkel at bruge:

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

Selvom de fleste implementeringer returnerer den anvendte buffermængde.

Valgmulighed 2 vil være langsommere og er farlig, når du bruger dynamisk sammenkædede biblioteker, hvor de kan bruge forskellige driftstider (og forskellige dynger). Så du kan ikke frigøre, hvad der er blevet malokeret i et andet bibliotek. Dette kræver derefter en free_string(char*) -funktion for at håndtere den.

Kommentarer

  • Tak! Jeg synes også, jeg kan lide Alternativ 3 bedst. Jeg vil dog være i stand til at gøre ting som: printf("MyType: %s\n", mytype_to_string( mt, buf, sizeof(buf)); og derfor vinder jeg ‘ ikke som at returnere den anvendte længde, men snarere markøren til strengen. Den dynamiske bibliotekskommentar er virkelig vigtig.
  • Skal ‘ t dette være sizeof(buffer) - 1 for at tilfredsstille \0 terminator?
  • @ Michael-O nej null-udtrykket er inkluderet i bufferstørrelsen, hvilket betyder, at den maksimale streng, der kan sættes i, er 1 mindre end den beståede størrelse. Dette er det mønster, som den sikre streng fungerer i standardbiblioteket som snprintf brug.
  • @ratchetfreak Tak for afklaring. Ville være rart at udvide svaret med den visdom.

Svar

Yderligere designidee til # 3

Når det er muligt, skal du også angive den maksimale størrelse, der er nødvendig for mytype i samme .h-fil som mytype_to_string().

#define MYTYPE_TO_STRING_SIZE 256 

Nu kan brugeren kode i overensstemmelse hermed.

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

Bestil

Størrelsen af arrays tillader VLA-typer, når de først er.

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

Ikke så vigtigt med en enkelt dimension, men alligevel nyttigt med 2 eller flere.

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

Jeg kan huske at læse at have størrelsen først er et foretrukket udtryk i næste C. Behov for at finde den reference ….

Svar

Som en tilføjelse til @ratchetfreaks fremragende svar vil jeg påpege, at alternativ nr. 3 følger et lignende paradigme / mønster som standard C-biblioteksfunktioner.

For eksempel strncpy.

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

At følge det samme paradigme vil hjælpe for at reducere den kognitive belastning for nye udviklere (eller endda dit fremtidige selv), når de har brug for din funktion.

Den eneste forskel med hvad du har i dit indlæg ville være, at destination -argument i C-bibliotekerne har tendens til at blive anført først i argumentlisten.Så:

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

Svar

I “d echo @ratchet_freak i de fleste tilfælde (måske med en lille tweak til sizeof buffer over 128) men jeg vil springe ind her med et underligt svar. Hvad med at være underlig? Hvorfor ikke udover problemerne med at få underlige blik fra vores kolleger og være ekstra overbevisende? Og jeg tilbyder dette:

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

Og eksempel på brug:

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

Hvis du gjorde det på denne måde, kan du gøre din tildeler meget effektiv (mere effektiv end malloc både med hensyn til allokerings- / deallokationsomkostninger og også forbedret referencelokalitet for hukommelsesadgang. og samle hukommelsen sekventielt fra store sammenhængende blokke (hvor den første blok ikke engang kræver en bunke al placering – det kan tildeles på stakken). Det forenkler fejlhåndtering. Også, og dette kan være det mest diskutabelt, men jeg synes, det er praktisk talt indlysende med hensyn til, hvordan det gør det klart for den, der ringer op, at det vil allokere hukommelse til den alloker, du sender ind, hvilket kræver eksplicit frigørelse (allocator_purge i dette tilfælde) uden at skulle dokumentere sådan adfærd i hver eneste mulige funktion, hvis du bruger denne stil konsekvent. Accepten af tildelingsparameteren gør det forhåbentlig ganske indlysende.

Jeg ved det ikke. Jeg får modargumenter her som at implementere den enklest mulige arenaallokering mulig (brug bare maksimal tilpasning til alle anmodninger) og at håndtere det er for meget arbejde. Min stumpe tanke er, hvad er vi, Python-programmører? Kan lige så godt bruge Python, hvis ja. Brug Python, hvis disse detaljer ikke betyder noget. Jeg er seriøs. Jeg har haft mange C-programmeringskollegaer, der meget sandsynligt ville skrive ikke kun mere korrekt kode, men muligvis endnu mere effektiv med Python, da de ignorerer ting som referencelokalitet, mens de snubler over bugs, de skaber til venstre og højre. Jeg don ” ikke se, hvad der er, der er så skræmmende ved en simpel arenaalloker her, hvis vi er C-programmører, der er bekymrede over ting som datalokalitet og optimalt instruktionsvalg, og det er uden tvivl meget mindre at tænke på end i det mindste de typer grænseflader, der kræver opkaldere til eksplicit at frigøre hver eneste individuelle ting, som grænsefladen kan returnere. Det tilbyder bulk-deallocation frem for individuel deallocation af en slags, der er mere udsat for fejl. En ordentlig C-programmør udfordrer loopy-opkald til malloc som jeg ser det, især når det ser ud som et hotspot i deres profil. Fra mit synspunkt skal der være mere ” oomph ” til en begrundelse for stadig at være C-programmør i 2020, og vi kan ” t viger væk fra ting som hukommelsesallokatorer længere.

Og dette har ikke kanttilfælde med at tildele en buffer i fast størrelse, hvor vi allokerer, fx 256 byte, og den resulterende streng er større. Selvom vores funktioner undgår bufferoverskridelser (som med sprintf_s), er der mere kode for korrekt at gendanne fra sådanne fejl, der kræves, som vi kan udelade med tildelersagen, da den ikke “t har disse kanttilfælde. Vi behøver ikke håndtere sådanne tilfælde i tildelingssagen, medmindre vi virkelig udtømmer det fysiske adresseringsrum af vores hardware (som ovenstående kode håndterer, men det behøver ikke at beskæftige sig med ” ud af forud tildelt buffer ” separat fra ” uden hukommelse “).

Svar

Udover det faktum, at hvad du foreslår at gøre er dårlig kodelugt, alternativ 3 lyder bedst for mig. Jeg synes ligesom @ gnasher729 at du bruger det forkerte sprog.

Kommentarer

Svar

For at være ærlig vil du måske skifte til et andet sprog, hvor returnering af en streng ikke er en kompleks, arbejdeintensiv og fejlbehæftet handling.

Du kan overveje C ++ eller Objective-C, hvor du kan lade 99% af din kode være uændret.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *