Perché molte funzioni che restituiscono strutture in C, restituiscono effettivamente i puntatori alle strutture?


49

Qual è il vantaggio di restituire un puntatore a una struttura anziché restituire l'intera struttura returnnell'istruzione della funzione?

Sto parlando di funzioni come fopene 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 progettuale piuttosto che una semplice questione di programmazione e sono curioso di sapere di più sui vantaggi e gli svantaggi dei due metodi.

Uno dei motivi per cui ho pensato che sarebbe stato un vantaggio restituire un puntatore a una struttura è poter dire più facilmente se la funzione falliva restituendo il NULLpuntatore.

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


10
@ JohnR.Strohm L'ho provato e funziona davvero. Una funzione può restituire una struttura ... Quindi qual è la ragione non viene eseguita?
yoyo_fun,

28
La pre-standardizzazione C non ha consentito la copia o il passaggio di strutture per valore. La libreria C standard ha molti ritardi di quell'epoca che non sarebbero stati scritti in quel modo oggi, ad esempio ci è voluto fino a C11 per rimuovere completamente la gets()funzione male progettata . Alcuni programmatori hanno ancora un'avversione per copiare le strutture, le vecchie abitudini sono dure a morire.
amon

26
FILE*è effettivamente una maniglia opaca. Il codice utente non dovrebbe preoccuparsi di quale sia la sua struttura interna.
CodesInChaos,

3
La restituzione per riferimento è solo un valore predefinito ragionevole quando si dispone di Garbage Collection.
Idan Arye,

7
@ JohnR.Strohm Il "molto senior" nel tuo profilo sembra risalire prima del 1989 ;-) - quando ANSI C ha permesso ciò che K&R C non ha fatto: copiare le strutture nelle assegnazioni, passare i parametri e restituire i valori. Il libro originale di K&R affermava infatti esplicitamente (sto parafrasando): "puoi fare esattamente due cose con una struttura, prendere il suo indirizzo con & e accedere a un membro con .".
Peter - Ripristina Monica il

Risposte:


61

Esistono diversi motivi pratici per cui funzioni come i fopenpuntatori di ritorno anziché le istanze di structtipi:

  1. Vuoi nascondere structall'utente la rappresentazione del tipo;
  2. Stai allocando un oggetto in modo dinamico;
  3. Ti riferisci a una singola istanza di un oggetto tramite più riferimenti;

Nel caso di tipi come FILE *, è perché non vuoi esporre all'utente i dettagli della rappresentazione del tipo: un FILE *oggetto funge da handle opaco e lo passi solo a varie routine di I / O (e mentre FILEspesso è implementato come un structtipo, non deve essere).

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

typedef struct __some_internal_stream_implementation FILE;

Sebbene non sia possibile dichiarare un'istanza di un tipo incompleto, è possibile dichiarare un puntatore ad essa. Quindi posso creare un FILE *e assegnarglielo attraverso fopen, freopenecc., Ma non posso manipolare direttamente l'oggetto a cui punta.

È anche probabile che la fopenfunzione stia allocando un FILEoggetto in modo dinamico, usando malloco simili. In tal caso, ha senso restituire un puntatore.

Infine, è possibile che tu stia memorizzando un tipo di stato in un structoggetto e devi renderlo disponibile in diversi luoghi. Se restituissi istanze del structtipo, tali istanze sarebbero oggetti separati in memoria l'uno dall'altro e alla fine non verrebbero sincronizzati. Restituendo un puntatore a un singolo oggetto, tutti si riferiscono allo stesso oggetto.


31
Un vantaggio particolare dell'utilizzo del puntatore come tipo opaco è che la struttura stessa può cambiare tra le versioni della libreria e non è necessario ricompilare i chiamanti.
Barmar,

6
@Barmar: In effetti, ABI Stability è l' enorme punto di forza di C, e non sarebbe così stabile senza puntatori opachi.
Matthieu M.,

37

Esistono due modi per "restituire una struttura". È possibile restituire una copia dei dati oppure è possibile restituire un riferimento (puntatore) ad esso. In genere si preferisce restituire (e passare in generale) un puntatore, per un paio di motivi.

Innanzitutto, la copia di una struttura richiede molto più tempo della CPU rispetto alla copia di un puntatore. Se questo è qualcosa che il tuo codice fa frequentemente, può causare una notevole differenza di prestazioni.

In secondo luogo, non importa quante volte copi un puntatore, punta ancora alla stessa struttura in memoria. Tutte le modifiche ad esso verranno riflesse 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à la modifica. A volte, molto raramente, questo è quello che vuoi, ma il più delle volte non lo è, e può causare bug se sbagli.


54
Lo svantaggio di tornare con il puntatore: ora devi tracciare la proprietà di quell'oggetto e possibilmente liberarlo. Inoltre, l'indirizzamento indiretto del puntatore potrebbe essere più costoso di una copia rapida. Ci sono molte variabili qui, quindi usare i puntatori non è universalmente migliore.
amon

17
Inoltre, i puntatori in questi giorni sono 64 bit sulla maggior parte delle piattaforme desktop e server. Ho visto più di alcune strutture nella mia carriera che si adattavano a 64 bit. Quindi, non si può sempre dire che la copia di un puntatore costa meno della copia di una struttura.
Solomon Slow

37
Questa è principalmente una buona risposta, ma non sono d'accordo sulla parte a volte, molto raramente, questo è quello che vuoi, ma il più delle volte non lo è, anzi il contrario. Restituire un puntatore consente a diversi tipi di effetti collaterali indesiderati e diversi tipi di modi cattivi di sbagliare la proprietà di un puntatore. Nei casi in cui il tempo della CPU non è così importante, preferisco la variante di copia, se questa è un'opzione, è molto meno soggetta a errori.
Doc Brown,

6
Va notato che questo vale davvero solo per le API esterne. Per le funzioni interne ogni compilatore anche marginalmente competente degli ultimi decenni riscriverà una funzione che restituisce una grande struttura per prendere un puntatore come argomento aggiuntivo e costruire l'oggetto direttamente lì. Le argomentazioni di immutabile vs mutevole sono state fatte abbastanza spesso, ma penso che possiamo tutti concordare sul fatto che l'affermazione che le strutture di dati immutabili non sono quasi mai ciò che si desidera non è vera.
Voo

6
Potresti anche menzionare i muri di fuoco della compilation come un professionista per i puntatori. Nei programmi di grandi dimensioni con intestazioni ampiamente condivise, tipi incompleti con funzioni impediscono la necessità di ricompilare ogni volta che cambia un dettaglio dell'implementazione. Il miglior comportamento di compilazione è in realtà un effetto collaterale dell'incapsulamento che si ottiene quando l'interfaccia e l'implementazione sono separate. Per restituire (e passare, assegnare) in base al valore sono necessarie le informazioni di implementazione.
Peter - Ripristina Monica il

12

Oltre ad altre risposte, a volte vale la pena restituire un valore ridotto struct . Ad esempio, si potrebbe restituire una coppia di dati e un codice di errore (o di successo) ad esso correlato.

Per fare un esempio, fopenrestituisce solo un dato (quello aperto FILE*) e, in caso di errore, fornisce il codice di errore attraverso la errnovariabile pseudo-globale. Ma sarebbe forse meglio restituire uno structdei due membri: l' FILE*handle e il codice di errore (che sarebbe impostato se l'handle del file è NULL). Per ragioni storiche non è così (e gli errori sono segnalati attraverso il errnoglobale, che oggi è una macro).

Si noti che la lingua Go ha una buona notazione per restituire due (o pochi) valori.

Si noti inoltre che su Linux / x86-64 l' ABI e le convenzioni di chiamata (vedere la pagina x86-psABI ) specifica che uno structdei 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, la restituzione di una piccola C structpuò essere più leggibile, più intuitiva e più efficiente.


In realtà le strutture di piccole dimensioni sono imballati in rdx:rax. Quindi struct foo { int a,b; };viene restituito imballato in rax(ad es. Con maiusc / o) e deve essere disimballato con maiusc / mov. Ecco un esempio su Godbolt . Ma x86 può usare i 32 bit bassi di un registro a 64 bit per le operazioni a 32 bit senza preoccuparsi dei bit alti, quindi è sempre troppo male, ma sicuramente peggio dell'utilizzo di 2 registri la maggior parte delle volte per le strutture a 2 membri.
Peter Cordes,

Correlato: bugs.llvm.org/show_bug.cgi?id=34840 std::optional<int> restituisce il valore booleano nella metà superiore di rax, quindi è necessaria una costante di maschera a 64 bit per testarlo test. Oppure potresti usare bt. Ma fa schifo per il chiamante e la chiamata rispetto all'utilizzo dl, cosa che i compilatori dovrebbero fare per le funzioni "private". Anche correlato: libstdc ++ std::optional<T>non è banalmente copiabile anche quando T lo è, quindi ritorna sempre tramite puntatore nascosto: stackoverflow.com/questions/46544019/… . (libc ++ è banalmente copiabile)
Peter Cordes,

@PeterCordes: le tue cose correlate sono C ++, non C
Basile Starynkevitch

Oops, giusto. Bene, 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 ++ banalmente copiabili usano lo stesso ABI di C.
Peter Cordes,

1
Esempio classicodiv_t div()
chux - Ripristina Monica il

6

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 è quello di poter dire più facilmente se la funzione non è riuscita restituendo il 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 fare una copia ogni volta che si desidera fare riferimento a esso. Invece, se si passa semplicemente attorno a un puntatore per fare riferimento alla trama, il programma verrà eseguito molto più velocemente.

Il motivo principale è però l'allocazione dinamica della memoria. Spesso, quando viene compilato un programma, non si è sicuri della quantità di memoria necessaria per determinate strutture di dati. Quando ciò accade, la quantità di memoria che è necessario 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ò è la lettura da un file specificato dall'utente. In questo caso, non hai idea di quanto possa essere grande il file quando compili il programma. Puoi capire quanta memoria hai bisogno quando il programma è effettivamente in esecuzione.

Sia malloc che i puntatori gratuiti restituiscono le posizioni in memoria. Pertanto, le funzioni che utilizzano l'allocazione dinamica della memoria restituiranno i puntatori a dove hanno creato le loro strutture in memoria.

Inoltre, nei commenti vedo che c'è una domanda sul fatto che è possibile restituire una struttura da una funzione. Puoi davvero farlo. Il seguente 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;
}

Come è possibile non sapere quanta memoria sarà necessaria una determinata variabile se si è già definito il tipo di struttura?
yoyo_fun,

9
@JenniferAnderson C ha un concetto di tipi incompleti: un nome di tipo può essere dichiarato ma non ancora definito, quindi la sua 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 un'intestazione, ma definire solo le strutture all'interno di un file C, consentendo così l'incapsulamento.
amon

@amon Quindi, ecco come dichiarare le intestazioni delle funzioni (prototipi / firme) prima di dichiarare come funzionano effettivamente in C? Ed è possibile fare la stessa cosa con le strutture e i sindacati in C
yoyo_fun

@JenniferAnderson dichiari i prototipi di funzione (funzioni senza corpi) nei file di intestazione e puoi quindi chiamare quelle funzioni in un altro codice, senza conoscere il corpo delle funzioni, perché il compilatore deve solo sapere come organizzare gli argomenti e come accettare il valore di ritorno. Quando colleghi il programma, devi effettivamente conoscere la definizione della funzione (cioè con un corpo), ma devi elaborarla una sola volta. Se si utilizza un tipo non semplice, è necessario conoscere anche la struttura di quel tipo, ma i puntatori hanno spesso le stesse dimensioni e non importa per l'uso di un prototipo.
simpleuser,

6

Qualcosa come un FILE*non è in realtà un puntatore a una struttura per quanto riguarda il codice client, ma è invece una forma di identificatore opaco associato ad altre entità come un file. Quando un programma chiama fopen, in genere non si preoccuperà di nessuno dei contenuti della struttura restituita - tutto ciò che gli interesserà è che altre funzioni come freadfaranno tutto ciò di cui hanno bisogno.

Se una libreria standard mantiene FILE*informazioni su, ad esempio, la posizione di lettura corrente all'interno di quel file, una chiamata a freaddovrebbe essere in grado di aggiornare tali informazioni. Avere freadricevuto un puntatore a FILErende così facile. Se freadinvece ricevesse un a FILE, non avrebbe modo di aggiornare l' FILEoggetto trattenuto dal chiamante.


3

Nascondere le informazioni

Qual è il vantaggio di restituire un puntatore a una struttura anziché restituire l'intera struttura nell'istruzione return della funzione?

Il più comune è nascondere le informazioni . C non ha, per esempio, la capacità di creare campi di un structprivato, e tanto meno di fornire metodi per accedervi.

Quindi, se si desidera impedire con forza agli sviluppatori di essere in grado di vedere e manomettere i contenuti di una punta, FILEquindi, l'unico modo è impedire che vengano esposti alla sua definizione trattando il puntatore come opaco le cui dimensioni e le definizioni sono sconosciute al mondo esterno. La definizione di FILEsarà quindi visibile solo a coloro che implementano le operazioni che richiedono la sua definizione, come fopen, mentre solo la dichiarazione di struttura sarà visibile all'intestazione pubblica.

Compatibilità binaria

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

Ad esempio, posso effettivamente eseguire alcuni programmi antichi creati durante l'era 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 dall'era di Windows 95. Tuttavia, i programmi continuano a funzionare con nuove versioni di Windows poiché non erano esposti al contenuto di tali strutture. Quando si lavora su una libreria in cui la compatibilità binaria è importante, ciò a cui il client non è esposto è generalmente permesso di cambiare senza interrompere la compatibilità all'indietro.

Efficienza

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

È in genere meno efficiente supponendo che il tipo possa praticamente adattarsi ed essere allocato nello stack a meno che non ci sia un allocatore di memoria molto meno generalizzato utilizzato dietro le quinte rispetto a malloc, come una memoria di pool di allocatori di dimensioni fisse anziché variabili già allocata. È un compromesso di sicurezza in questo caso, molto probabilmente, per consentire agli sviluppatori di biblioteche di mantenere invarianti (garanzie concettuali) FILE.

Non è un motivo così valido almeno dal punto di vista delle prestazioni per rendere fopenun puntatore restituito poiché l'unico motivo che restituirebbe NULLè la mancata apertura di un file. Ciò sarebbe l'ottimizzazione di uno scenario eccezionale in cambio del rallentamento di tutti i percorsi di esecuzione del caso comune. In alcuni casi potrebbe esserci un valido motivo di produttività per rendere i progetti più semplici per renderli puntatori di ritorno per consentire NULLdi essere restituiti in alcune condizioni post.

Per le operazioni sui file, l'overhead è relativamente banale rispetto alle operazioni sui file stessi e il manuale fclosenon deve essere comunque evitato. Quindi non è come se potessimo salvare al cliente la seccatura di liberare (chiudere) la risorsa esponendo la definizione FILEe restituendola per valore fopeno aspettandoci molto di un aumento delle prestazioni dato il costo relativo delle operazioni sui file stessi per evitare un'allocazione dell'heap .

Hotspot e correzioni

Per altri casi, tuttavia, ho profilato un sacco di codice C dispendioso in basi di codice legacy con hotspot dentro malloce inutili mancate cache obbligatorie come risultato dell'utilizzo di questa pratica troppo frequentemente con puntatori opachi e allocando troppe cose inutilmente sull'heap, a volte in grandi anelli.

Una pratica alternativa che uso invece è quella di esporre le definizioni della struttura, anche se il cliente non ha lo scopo di manometterle, utilizzando uno standard della convenzione di denominazione per comunicare che nessun altro dovrebbe 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 l'ho trovato abbastanza buono da riservare in modo superfluo dello spazio aggiuntivo 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;
};

Lo spazio riservato è un po 'dispendioso, ma può essere un salvavita se in futuro dovessimo aggiungere altri dati Foosenza rompere i binari che utilizzano la nostra libreria.

A mio avviso, nascondere le informazioni e la compatibilità binaria è in genere l'unica ragione decente per consentire l'allocazione di heap di strutture oltre a strutture a lunghezza variabile (che richiederebbe sempre, o almeno essere un po 'scomodo da usare altrimenti se il client dovesse allocare memoria nello stack in modo VLA per allocare il VLS). Anche le grandi strutture sono spesso più economiche da restituire in base al valore se ciò significa che il software funziona molto di più con la memoria calda nello stack. E anche se non fossero più economici per tornare in base al valore sulla 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 Foodallo stack senza la possibilità di una copia superflua. Oppure il cliente ha anche la libertà di allocare Foosull'heap se lo desidera per qualche motivo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.