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 struct
privato, 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, FILE
quindi, 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 FILE
sarà 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 fopen
un 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 NULL
di essere restituiti in alcune condizioni post.
Per le operazioni sui file, l'overhead è relativamente banale rispetto alle operazioni sui file stessi e il manuale fclose
non deve essere comunque evitato. Quindi non è come se potessimo salvare al cliente la seccatura di liberare (chiudere) la risorsa esponendo la definizione FILE
e restituendola per valore fopen
o 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 malloc
e 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 Foo
senza 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 Foo
dallo stack senza la possibilità di una copia superflua. Oppure il cliente ha anche la libertà di allocare Foo
sull'heap se lo desidera per qualche motivo.