Funzioni che restituiscono stringhe, buon stile?

Nei miei programmi C spesso ho bisogno di un modo per creare una rappresentazione di stringa dei miei ADT. Anche se non ho bisogno di stampare la stringa sullo schermo in alcun modo, è carino avere un tale metodo per il debug. Quindi questo tipo di funzione spesso viene fuori.

char * mytype_to_string( const mytype_t *t ); 

Realmente mi rendo conto che ho (almeno) tre opzioni qui per gestire la memoria per la stringa da restituire.

Alternativa 1: memorizzare la stringa di ritorno in un array di caratteri statici nella funzione Non ho bisogno di pensare molto, tranne che la stringa viene sovrascritta ad ogni chiamata. Il che potrebbe essere un problema in alcune occasioni.

Alternativa 2: alloca la stringa nellheap con malloc allinterno della funzione. Veramente pulito dato che allora non ho bisogno di pensare alla dimensione di un buffer o alla sovrascrittura. Tuttavia, devo ricordarmi di liberare () la stringa quando ho finito, e poi devo anche assegnarla a una variabile temporanea tale che Posso liberare. E quindi lallocazione dellheap è molto più lenta dellallocazione dello stack, quindi essere un collo di bottiglia se questo viene ripetuto in un ciclo.

Alternativa 3: passare il puntatore a un buffer e lasciare che il chiamante allochi quel buffer. Come:

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

Questo porta più impegno al chiamante. Ho anche notato che questa alternativa mi dà unaltra opzione sullordine degli argomenti. Quale argomento dovrei avere per primo e per ultimo? (In realtà sei possibilità)

Quindi, quale dovrei preferire? Qualche motivo? Cè una sorta di standard non scritto tra gli sviluppatori C?

Commenti

  • Solo una nota osservativa, la maggior parte dei sistemi operativi usa lopzione 3 – il chiamante alloca il buffer in ogni caso; dice al puntatore del buffer e alla capacità; il chiamato riempie il buff er e restituisce anche la lunghezza effettiva della stringa se il buffer è insufficiente. Esempio: sysctlbyname in OS X e iOS

Risposta

I metodi che ho visto di più sono 2 e 3.

Il buffer fornito dallutente è in realtà abbastanza semplice da usare:

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

Anche se la maggior parte delle implementazioni restituirà la quantità di buffer utilizzata.

Lopzione 2 sarà più lenta ed è pericolosa quando si utilizzano librerie collegate dinamicamente dove potrebbero essere utilizzate diversi runtime (e diversi heap). In questo modo non puoi liberare ciò che è stato allocato in unaltra libreria. Ciò richiede quindi una funzione free_string(char*) per gestirlo.

Commenti

  • Grazie! Penso che anche io preferisco Alternativa 3. Tuttavia voglio essere in grado di fare cose come: printf("MyType: %s\n", mytype_to_string( mt, buf, sizeof(buf)); e quindi non mi piace ‘ restituire la lunghezza usata ma piuttosto il puntatore alla stringa. Il commento della libreria dinamica è molto importante.
  • Non dovrebbe ‘ essere sizeof(buffer) - 1 per soddisfare il \0 terminator?
  • @ Michael-O no il termine null è incluso nella dimensione del buffer, il che significa che la stringa massima che può essere inserita è 1 in meno della dimensione passata. Questo è il modello utilizzato dalla stringa sicura nella libreria standard come snprintf.
  • @ratchetfreak Grazie per i chiarimenti. Sarebbe bello estendere la risposta con questa saggezza.

Rispondi

Idea di design aggiuntiva per

Se possibile, fornisci anche la dimensione massima necessaria per mytype nello stesso file .h di mytype_to_string().

#define MYTYPE_TO_STRING_SIZE 256 

Ora lutente può codificare di conseguenza.

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

Ordina

La dimensione degli array, quando è la prima, consente i tipi VLA.

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

Non così importante con una dimensione singola, ma utile con 2 o più.

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

Ricordo di aver letto avere prima la dimensione è un idioma preferito nel prossimo C. Necessità di trovare quel riferimento ….

Risposta

Come aggiunta alleccellente risposta di @ratchetfreak, vorrei sottolineare che lalternativa n. 3 segue un paradigma / modello simile a quello delle funzioni di libreria C standard.

Ad esempio, strncpy.

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

Seguire lo stesso paradigma sarebbe utile per ridurre il carico cognitivo per i nuovi sviluppatori (o anche per te stesso futuro) quando hanno bisogno di utilizzare la tua funzione.

Lunica differenza con ciò che hai nel tuo post sarebbe che il nelle librerie C tende ad essere elencato per primo nellelenco degli argomenti.Quindi:

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

Risposta

I “d echo @ratchet_freak nella maggior parte dei casi (forse con una piccola modifica per sizeof buffer su 128) ma voglio saltare qui con una risposta strana. Che ne dici di essere strano? Perché no, oltre ai problemi di ottenere sguardi strani dai nostri colleghi e dover essere più persuasivi? E io offro questo:

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

E un esempio di utilizzo:

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

Se lhai fatto in questo modo, puoi rendere il tuo allocatore molto efficiente (più efficiente rispetto a malloc sia in termini di costi di allocazione / deallocazione che anche di località di riferimento migliorata per laccesso alla memoria). Può essere un allocatore di arena che comporta semplicemente lincremento di un puntatore in casi comuni per le richieste di allocazione e pool di memoria in modo sequenziale da grandi blocchi contigui (con il primo blocco che non richiede nemmeno un heap al posizione: può essere allocato nello stack). Semplifica la gestione degli errori. Inoltre, e questo potrebbe essere il più discutibile ma penso che sia praticamente abbastanza ovvio in termini di come rende chiaro al chiamante che sta per allocare memoria allallocatore che passi, richiedendo una liberazione esplicita (allocator_purge in questo caso) senza dover documentare tale comportamento in ogni singola funzione possibile se utilizzi questo stile in modo coerente. Laccettazione del parametro allocator lo rende, si spera, abbastanza ovvio.

Non lo so. Qui ottengo controargomentazioni come limplementazione dellallocatore di arena più semplice possibile (usa solo il massimo allineamento per tutte le richieste) e occuparsene è troppo faticoso. Il mio pensiero schietto è del tipo, cosa siamo, programmatori Python? Potremmo anche usare Python se è così. Per favore usa Python se questi dettagli non contano. Dico sul serio. Ho avuto molti colleghi di programmazione in C che molto probabilmente scriverebbero non solo codice più corretto ma forse anche più efficiente con Python poiché ignorano cose come la località di riferimento mentre incappano in bug che creano a destra ea sinistra. Non vedo cosa cè di così spaventoso in un semplice allocatore di arena qui se siamo programmatori C interessati a cose come la località dei dati e la selezione ottimale delle istruzioni, e questo è probabilmente molto meno a cui pensare rispetto almeno ai tipi di interfacce che richiedono chiamanti per liberare esplicitamente ogni singola cosa individuale che linterfaccia può restituire. Offre la deallocazione in blocco rispetto alla deallocazione individuale di un tipo che è più soggetto a errori. Un programmatore C appropriato sfida le chiamate inaffidabili a malloc come la vedo io, specialmente quando appare come un punto caldo nel loro profiler. Dal mio punto di vista, deve esserci più ” oomph ” una motivazione per essere ancora un programmatore C nel 2020, e possiamo ” Non rifuggire più da cose come gli allocatori di memoria.

E questo non ha i casi limite di allocare un buffer di dimensioni fisse in cui allochiamo, diciamo, 256 byte e la stringa risultante è più grande. Anche se le nostre funzioni evitano sovraccarichi del buffer (come con sprintf_s), cè più codice per recuperare correttamente da tali errori che è necessario che possiamo omettere con il caso dellallocatore poiché non “Non abbiamo questi casi limite. Non dobbiamo occuparci di questi casi nel caso dellallocatore, a meno che non esauriamo veramente lo spazio di indirizzamento fisico del nostro hardware (che il codice precedente gestisce, ma non ha a che fare con ” buffer preallocato ” separatamente da ” memoria esaurita “).

Risposta

Oltre al fatto che ciò che si propone di fare è un cattivo odore di codice, lalternativa 3 suona meglio per me. Penso anche come @ gnasher729 che tu stia usando la lingua sbagliata.

Commenti

Risposta

Ad essere onesti, potresti voler passare a una lingua diversa in cui la restituzione di una stringa non è unoperazione complessa, laboriosa e soggetta a errori.

Potresti considerare C ++ o Objective-C dove potresti lasciare il 99% del tuo codice invariato.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *