Perché molte funzioni che restituiscono strutture in C, in realtà restituiscono puntatori a strutture?

Qual è il vantaggio di restituire un puntatore a una struttura rispetto a restituire lintera struttura nel return dichiarazione della funzione?

Sto parlando di funzioni come fopen e altre funzioni di basso livello ma probabilmente ci sono funzioni di livello superiore che restituiscono anche puntatori a strutture.

Credo che questa sia più una scelta di design piuttosto che una semplice domanda di programmazione e sono curioso di saperne di più sui vantaggi e gli svantaggi dei due metodi.

Uno dei I motivi per cui ho pensato che sarebbe stato un vantaggio restituire un puntatore a una struttura è essere in grado di dire più facilmente se la funzione non è riuscita restituendo il puntatore NULL.

Restituire una struttura completa che è NULL suppongo sarebbe più difficile o meno efficiente. È un motivo valido?

Commenti

  • @ JohnR.Strohm Lho provato e funziona davvero. Una funzione può restituire una struttura …. Allora qual è il motivo per cui non viene eseguita?
  • Pre-standardizzazione C non permetteva di copiare o passare per valore le strutture. La libreria standard C ha molte resistenze di quellera che non sarebbero scritte in questo modo oggi, ad es. ci è voluto fino al C11 per rimuovere la funzione gets() completamente mal progettata. Alcuni programmatori hanno ancora unavversione per la copia di strutture, le vecchie abitudini sono dure a morire.
  • FILE* è effettivamente un handle opaco. Il codice utente non dovrebbe preoccuparsi di quale sia la sua struttura interna.
  • La restituzione per riferimento è solo un valore predefinito ragionevole quando si dispone della raccolta dei rifiuti.
  • @ JohnR.Strohm Il ” very senior ” nel tuo profilo sembra risalire a prima del 1989 😉 – quando ANSI C consentiva ciò che K & RC didn ‘ t: Copia le strutture nelle assegnazioni, nel passaggio di parametri e nei valori restituiti. Il libro originale di K & R ‘ è effettivamente dichiarato esplicitamente (io ‘ parafrasando): ” puoi fare esattamente due cose con una struttura, prendere il suo indirizzo con & e accedere a un membro con .. ”

Rispondi

Lì ci sono diversi motivi pratici per cui funzioni come fopen restituiscono puntatori invece di istanze di struct tipi:

  1. Desideri nascondere allutente la rappresentazione del tipo struct;
  2. Stai allocando un oggetto in modo dinamico;
  3. Stai riferendosi a una singola istanza di un oggetto tramite più riferimenti;

Nel caso di tipi come FILE *, è perché non lo fai desidera esporre i dettagli della rappresentazione del tipo allutente – un FILE * obje ct funge da handle opaco e basta passare quellhandle a varie routine di I / O (e mentre FILE è spesso implementato come struct tipo, non deve essere).

Quindi, puoi esporre un tipo incompleto struct in unintestazione da qualche parte:

typedef struct __some_internal_stream_implementation FILE; 

Sebbene non sia possibile dichiarare unistanza di un tipo incompleto, è possibile dichiarare un puntatore ad essa. Quindi posso creare un FILE * e assegnarlo tramite fopen, freopen e così via. , ma non posso manipolare direttamente loggetto a cui punta.

È anche probabile che la funzione fopen stia allocando un FILE oggetto in modo dinamico, utilizzando malloc o simile. In tal caso, ha senso restituire un puntatore.

Infine, è possibile che tu stia memorizzando un qualche tipo di stato in un oggetto struct e devi rendere quello stato disponibile in molti posti differenti. Se restituissi istanze del tipo struct, tali istanze sarebbero oggetti separati in memoria luno dallaltro e alla fine perderebbero la sincronizzazione. Restituendo un puntatore a un singolo oggetto, tutti si riferiscono allo stesso oggetto.

Commenti

  • Un vantaggio particolare nellusare il puntatore come il tipo opaco è che la struttura stessa può cambiare tra le versioni della libreria e non ‘ è necessario ricompilare i chiamanti.
  • @Barmar: In effetti, la stabilità ABI è lenorme punto di forza del C, e non sarebbe così stabile senza puntatori opachi.

Risposta

Ci sono due modi per” restituire una struttura “. Puoi restituire una copia dei dati, oppure puoi restituire un riferimento (puntatore) ad essa.In genere si preferisce restituire (e passare in giro in generale) un puntatore, per un paio di motivi.

Primo, copiare una struttura richiede molto più tempo della CPU che copiare un puntatore. Se questo è qualcosa il tuo codice fa spesso, può causare una notevole differenza di prestazioni.

Secondo, non importa quante volte copi un puntatore, esso punta ancora alla stessa struttura in memoria. Tutte le modifiche ad esso si rifletteranno sulla stessa struttura. Ma se copi la struttura stessa e poi apporti una modifica, la modifica viene visualizzata solo su quella copia . Qualsiasi codice che contiene una copia diversa non vedrà il cambiamento. A volte, molto raramente, questo è quello che vuoi, ma la maggior parte delle volte non lo è e può causare bug se lo sbagli.

Commenti

  • Lo svantaggio della restituzione tramite puntatore: ora ‘ devi tenere traccia della proprietà di quelloggetto ed è possibile liberalo. Inoltre, lindirizzamento indiretto del puntatore può essere più costoso di una copia veloce. Ci sono molte variabili qui, quindi usare i puntatori non è universalmente migliore.
  • Inoltre, i puntatori oggigiorno sono 64 bit sulla maggior parte delle piattaforme desktop e server. ‘ ho visto più di alcune strutture nella mia carriera che si adattano a 64 bit. Quindi, puoi ‘ t sempre dire che copiare un puntatore costa meno che copiare una struttura.
  • Questa è principalmente una buona risposta , ma non sono daccordo sulla parte a volte, molto raramente, è quello che vuoi, ma la maggior parte delle volte ‘ non – al contrario. La restituzione di un puntatore consente diversi tipi di effetti collaterali indesiderati e diversi tipi di modi sgradevoli per ottenere la proprietà di un puntatore sbagliata. Nei casi in cui il tempo della CPU non è così importante, preferisco la variante di copia, se questa è unopzione, è molto meno soggetta a errori.
  • Va notato che questo si applica solo alle API esterne. Per le funzioni interne, ogni compilatore anche marginalmente competente degli ultimi decenni riscriverà una funzione che restituisce una struttura grande per prendere un puntatore come argomento aggiuntivo e costruire loggetto direttamente lì. Gli argomenti di immutabile vs mutabile sono stati fatti abbastanza spesso, ma penso che tutti possiamo essere daccordo sul fatto che laffermazione che le strutture di dati immutabili non sono quasi mai ciò che desideri non è vera.
  • Potresti anche menzionare i firewall delle compilation come un professionista per i puntatori. In programmi di grandi dimensioni con intestazioni ampiamente condivise tipi incompleti con funzioni impediscono la necessità di ricompilare ogni volta che cambia un dettaglio di implementazione. Il miglior comportamento di compilazione è in realtà un effetto collaterale dellincapsulamento che si ottiene quando linterfaccia e limplementazione sono separate. Restituire (e passare, assegnare) per valore necessita delle informazioni di implementazione.

Risposta

Oltre ad altre risposte , a volte è utile restituire un piccolo struct per valore. Ad esempio, si potrebbe restituire una coppia di un dato e un codice di errore (o di successo) ad esso correlato.

Per fare un esempio, fopen restituisce solo un dato (il FILE* aperto) e in caso di errore fornisce il codice di errore tramite errno variabile pseudo-globale. Ma forse sarebbe meglio restituire un struct di due membri: lhandle FILE* e il codice di errore (che verrebbe impostato se lhandle del file è NULL). Per ragioni storiche non è così (e gli errori vengono segnalati tramite il errno globale, che oggi è una macro).

Si noti che Go language ha una bella notazione per restituire due (o pochi) valori.

Si noti inoltre che su Linux / x86-64 ABI e le convenzioni di chiamata (vedere la pagina x86-psABI ) specifica che un struct di due membri scalari (ad esempio un puntatore e un numero intero, o due puntatori o due numeri interi) viene restituito attraverso due registri (e questo è molto efficiente e non passa attraverso la memoria).

Quindi, nel nuovo codice C, restituire un piccolo C struct può essere più leggibile, più adatto ai thread e più efficiente.

Commenti

  • In realtà le strutture piccole sono impacchettate in rdx:rax. Quindi struct foo { int a,b; }; viene restituito impacchettato in rax (ad es. Con shift / o) e deve essere decompresso con shift / mov. Qui ‘ è un esempio su Godbolt . Ma x86 può usare i 32 bit bassi di un registro a 64 bit per operazioni a 32 bit senza preoccuparsi dei bit alti, quindi ‘ è sempre un peccato, ma decisamente peggio dellutilizzo 2 si registra il più delle volte per strutture a 2 membri.
  • Correlato: bugs.llvm.org/show_bug.cgi? id = 34840 std::optional<int> restituisce il valore booleano nella metà superiore di rax, quindi è necessaria una maschera a 64 bit costante per verificarlo con test. Oppure puoi utilizzare bt. Ma fa schifo per il chiamante e il chiamato rispetto allutilizzo di dl, che i compilatori dovrebbero fare per ” private ” funzioni. Correlato anche: libstdc ++ ‘ s std::optional<T> non è ‘ t banalmente copiabile anche quando T è , quindi restituisce sempre tramite puntatore nascosto: stackoverflow.com/questions/46544019/… . (libc ++ ‘ s è facilmente copiabile)
  • @PeterCordes: le tue cose correlate sono C ++, non C
  • Oops, giusto. Ebbene, la stessa cosa si applicherebbe esattamente a struct { int a; _Bool b; }; in C, se il chiamante volesse testare il booleano, perché le strutture C ++ facilmente copiabili usano lo stesso ABI di C.
  • Esempio classico div_t div()

Risposta

Sei sulla strada giusta

Entrambi i motivi che hai citato sono validi:

Uno dei motivi per cui ho pensato che sarebbe un vantaggio restituire un puntatore a una struttura è essere in grado di dire più facilmente se la funzione fallisce restituendo un puntatore NULL.

Restituire una struttura FULL che è NULL sarebbe più difficile suppongo o meno efficiente. È un motivo valido?

Se hai una trama (per esempio) da qualche parte nella memoria e vuoi fare riferimento a quella trama in diversi punti del tuo programma; non sarebbe saggio farne una copia ogni volta che si desidera fare riferimento ad esso. Invece, se si passa semplicemente un puntatore per fare riferimento alla trama, il programma verrà eseguito molto più velocemente.

Il motivo principale però è lallocazione dinamica della memoria. Spesso, quando un programma viene compilato, non sei sicuro di quanta memoria hai bisogno per determinate strutture di dati. Quando ciò accade, la quantità di memoria che devi utilizzare verrà determinata in fase di esecuzione. Puoi richiedere la memoria usando malloc e poi liberarla quando hai finito di usare free.

Un buon esempio di ciò è leggere da un file specificato dallutente. In questo caso, non hai idea di quanto può essere grande il file quando compili il programma. Puoi solo capire quanta memoria ti serve quando il programma è effettivamente in esecuzione.

Sia malloc che free restituiscono puntatori a posizioni in memoria. Quindi funzioni che fanno uso dellallocazione dinamica della memoria restituiranno i puntatori a dove hanno creato le loro strutture in memoria.

Inoltre, nei commenti vedo che cè una domanda se puoi restituire una struttura da una funzione. Puoi davvero farlo. Dovrebbe funzionare:

struct s1 { int integer; }; struct s1 f(struct s1 input){ struct s1 returnValue = xinput return returnValue; } int main(void){ struct s1 a = { 42 }; struct s1 b= f(a); return 0; } 

Commenti

  • Come è possibile non sapere quanta memoria sarà necessaria una certa variabile se hai già il tipo di struttura definito?
  • @JenniferAnderson C ha un concetto di tipi incompleti: un nome di tipo può essere dichiarato ma non ancora definito, quindi ‘ la dimensione non è disponibile. Non posso dichiarare variabili di quel tipo, ma posso dichiarare puntatori a quel tipo, ad es. struct incomplete* foo(void). In questo modo posso dichiarare le funzioni in unintestazione, ma definire solo le strutture allinterno di un file C, consentendo così lincapsulamento.
  • @amon Quindi questo è il modo in cui dichiarare le intestazioni delle funzioni (prototipi / firme) prima di dichiarare come esse il lavoro è effettivamente svolto in C? Ed è possibile fare la stessa cosa per le strutture e le unioni in C
  • @JenniferAnderson dichiari la funzione prototipi (funzioni senza corpi) nei file di intestazione e puoi quindi chiamare quelle funzioni in altro codice, senza conoscere il corpo delle funzioni, perché il compilatore deve solo sapere come disporre gli argomenti e come accettare il valore restituito. Quando colleghi il programma, devi effettivamente conoscere la funzione definizione (cioè con un corpo), ma devi elaborarla solo una volta. Se utilizzi un tipo non semplice, devi anche conoscere la struttura del tipo ‘, ma i puntatori hanno spesso la stessa dimensione e non ‘ è importante per un ‘ utilizzo di un prototipo.

Risposta

Qualcosa come un FILE* non è realmente un puntatore a una struttura per quanto riguarda il codice client, ma è invece una forma di identificatore opaco associato ad alcuni unaltra entità come un file. Quando un programma chiama fopen, generalmente non si preoccupa di nessuno dei contenuti della struttura restituita: tutto ciò che gli interessa è che altre funzioni come fread faranno tutto ciò di cui hanno bisogno.

Se una libreria standard conserva allinterno di FILE* informazioni su es. la posizione di lettura corrente allinterno di quel file, una chiamata a fread dovrebbe essere in grado di aggiornare quelle informazioni. Il fatto che fread riceva un puntatore a FILE rende tutto più semplice. Se fread ricevesse invece un FILE, non avrebbe modo di aggiornare loggetto FILE trattenuto dal chiamante.

Risposta

Informazioni nascoste

Qual è il vantaggio di restituire un puntatore a una struttura rispetto a restituire lintera struttura nellistruzione return di la funzione?

La più comune è nascondere le informazioni . Il C non ha, diciamo, la capacità di rendere privati i campi di un struct, figuriamoci fornire metodi per accedervi.

Quindi, se vuoi forzare impedire agli sviluppatori di essere in grado di vedere e manomettere il contenuto di una punta, come FILE, quindi lunico modo è impedire che vengano esposti alla sua definizione trattando il puntatore come opaco la cui dimensione e definizione delle punte sono sconosciute al mondo esterno. La definizione di FILE sarà quindi visibile solo a coloro che implementano le operazioni che richiedono la sua definizione, come fopen, mentre solo la dichiarazione della struttura sarà visibile allintestazione pubblica.

Compatibilità binaria

Nascondere la definizione della struttura può anche aiutare a fornire spazio per preservare la compatibilità binaria nelle API dylib. Consente agli implementatori della libreria di modificare i campi nella struttura opaca uro senza compromettere la compatibilità binaria con coloro che usano la libreria, poiché la natura del loro codice ha bisogno solo di sapere cosa possono fare con la struttura, non quanto è grande o quali campi ha.

Come un Ad esempio, posso effettivamente eseguire alcuni programmi antichi costruiti durante lera di Windows 95 oggi (non sempre perfettamente, ma sorprendentemente molti funzionano ancora). È probabile che parte del codice di quegli antichi binari usasse puntatori opachi a strutture le cui dimensioni e contenuti sono cambiati rispetto allera di Windows 95. Tuttavia i programmi continuano a funzionare nelle nuove versioni di Windows poiché non sono stati esposti al contenuto di quelle strutture. Quando si lavora su una libreria in cui la compatibilità binaria è importante, ciò a cui il client non è esposto è generalmente consentito di cambiare senza interruzioni compatibilità con le versioni precedenti.

Efficienza

Restituire una struttura completa che è NULL sarebbe più difficile suppongo o meno efficiente. È un motivo valido?

È tipicamente meno efficiente supporre che il tipo possa praticamente adattarsi ed essere allocato nello stack a meno che non ce ne sia di molto allocatore di memoria generalizzato utilizzato dietro le quinte rispetto a malloc, come un allocatore di dimensioni fisse anziché di dimensioni variabili che raggruppa la memoria già allocata. In questo caso, la maggior parte probabilmente, per consentire agli sviluppatori di librerie di mantenere invarianti (garanzie concettuali) relative a FILE.

Non è una ragione così valida almeno dal punto di vista delle prestazioni per fare in modo che fopen restituisca un puntatore poiché lunico motivo per cui “d return NULL è la mancata apertura di un file. Sarebbe lottimizzazione di uno scenario eccezionale in cambio del rallentamento di tutti i percorsi di esecuzione dei casi comuni. Potrebbe esserci una valida ragione di produttività in alcuni casi per rendere i progetti più semplici per far sì che restituiscano puntatori per consentire la restituzione di NULL in alcune post-condizioni.

Per le operazioni sui file, loverhead è relativamente abbastanza banale rispetto alle operazioni sui file stesse e la necessità manuale di fclose non può essere evitata comunque. Quindi non è che possiamo evitare al cliente il fastidio di liberare (chiudere) la risorsa esponendo la definizione di FILE e restituendola per valore in fopen o aspettarsi un notevole aumento delle prestazioni dato il costo relativo delle operazioni sui file stesse per evitare unallocazione di heap.

Hotspot e correzioni

Per altri casi, però, ho “visto un sacco di spreco di codice C in codebase legacy con hotspot in malloc e inutili errori di cache obbligatori come risultato delluso di questa pratica troppo frequentemente con puntatori opachi e dellallocazione di troppe cose inutilmente sullheap, a volte in grandi cicli.

Una pratica alternativa che utilizzo invece è quella di esporre le definizioni della struttura, anche se il cliente non intende manometterle, utilizzando uno standard di convenzione di denominazione per comunicare che nessun altro deve toccare i campi:

struct Foo { /* priv_* indicates that you shouldn"t tamper with these fields! */ int priv_internal_field; int priv_other_one; }; struct Foo foo_create(void); void foo_destroy(struct Foo* foo); void foo_something(struct Foo* foo); 

Se ci sono problemi di compatibilità binaria in futuro, allora “lho trovato abbastanza buono da riservare semplicemente un po di spazio extra per scopi futuri, in questo modo:

struct Foo { /* priv_* indicates that you shouldn"t tamper with these fields! */ int priv_internal_field; int priv_other_one; /* reserved for possible future uses (emergency backup plan). currently just set to null. */ void* priv_reserved; }; 

Quello spazio riservato è un po dispendioso ma può salvarci la vita se in futuro dovessimo aggiungere altri dati a Foo senza rompere i binari che usano la nostra libreria.

A mio parere nascondere le informazioni e la compatibilità binaria è in genere lunica ragione decente per consentire solo lallocazione di heap di strutture oltre a quelle a lunghezza variabile (che lo richiederebbero sempre, o almeno sarebbe un po scomodo da usare altrimenti se il client dovesse allocare memoria sullo stack in un flash VLA ione per allocare il VLS). Anche le strutture di grandi dimensioni sono spesso più economiche da restituire in base al valore se ciò significa che il software lavora molto di più con la memoria calda sullo stack. E anche se non fossero più economici da restituire in valore alla creazione, si potrebbe semplicemente fare questo:

int foo_create(struct Foo* foo); ... /* In the client code: */ struct Foo foo; if (foo_create(&foo)) { foo_something(&foo); foo_destroy(&foo); } 

… per inizializzare Foo dallo stack senza la possibilità di una copia superflua. Oppure il client ha anche la libertà di allocare Foo sullheap se lo desidera per qualche motivo.

Lascia un commento

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