Devo inizializzare le strutture C tramite parametro o in base al valore restituito? [chiuso]


33

La società in cui lavoro sta inizializzando tutte le loro strutture di dati attraverso una funzione di inizializzazione in questo modo:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
InitializeFoo(Foo* const foo){
   foo->a = x; //derived here based on other data
   foo->b = y; //derived here based on other data
   foo->c = z; //derived here based on other data
}

//initializing the structure  
Foo foo;
InitializeFoo(&foo);

Ho avuto qualche spinta indietro nel tentativo di inizializzare le mie strutture in questo modo:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
Foo ConstructFoo(int a, int b, int c){
   Foo foo;
   foo.a = a; //part of parameter input (inputs derived outside of function)
   foo.b = b; //part of parameter input (inputs derived outside of function)
   foo.c = c; //part of parameter input (inputs derived outside of function)
   return foo;
}

//initialize (or construct) the structure
Foo foo = ConstructFoo(x,y,z);

C'è un vantaggio l'uno rispetto all'altro?
Quale dovrei fare e come lo giustificherei come pratica migliore?


4
@gnat Questa è una domanda esplicita sull'inizializzazione della struttura. Quel thread incarna alcune delle stesse motivazioni che vorrei vedere applicate per questa particolare decisione progettuale.
Trevor Hickey,

2
@Jefffrey Siamo in C, quindi in realtà non possiamo avere metodi. Non è neanche sempre un insieme diretto di valori. A volte l'inizializzazione di una struttura è di ottenere valori (in qualche modo) ed esegue una logica per inizializzare la struttura.
Trevor Hickey,

1
@JacquesB Ho ottenuto "Ogni componente che costruisci sarà diverso dagli altri. Esiste una funzione Initialize () usata altrove per la struttura. Tecnicamente parlando, chiamarlo costruttore è fuorviante."
Trevor Hickey,

1
@TrevorHickey InitializeFoo()è un costruttore. L'unica differenza rispetto a un costruttore C ++ è che il thispuntatore viene passato in modo esplicito anziché implicito. Il codice compilato di InitializeFoo()e un C ++ corrispondente Foo::Foo()è esattamente lo stesso.
cmaster

2
Opzione migliore: smettere di usare C su C ++. Autowin.
Thomas Eding,

Risposte:


25

Nel 2 ° approccio non avrai mai un Foo parzialmente inizializzato. Mettere tutta la costruzione in un posto sembra un posto più sensato e ovvio.

Ma ... il 1 ° modo non è poi così male, ed è spesso usato in molte aree (c'è persino una discussione sul modo migliore per iniettare dipendenza, o iniezione di proprietà come la tua prima strada, o iniezione del costruttore come la seconda) . Né è sbagliato.

Quindi, se nessuno dei due è sbagliato e il resto dell'azienda utilizza l'approccio n. 1, allora dovresti adattarti alla base di codice esistente e non cercare di rovinarlo introducendo un nuovo modello. Questo è davvero il fattore più importante in gioco qui, gioca bene con i tuoi nuovi amici e non cercare di essere quel fiocco di neve speciale che fa le cose in modo diverso.


Ok, sembra ragionevole. Avevo l'impressione che l'inizializzazione di un oggetto senza poter vedere che tipo di input lo stava inizializzando, avrebbe portato alla confusione. Stavo cercando di seguire il concetto di data in / data out per produrre codice prevedibile e verificabile. Farlo diversamente sembrava aumentare l'accoppiamento poiché il file sorgente della mia struttura aveva bisogno di dipendenze extra per eseguire l'inizializzazione. Hai ragione però, in quanto non voglio scuotere la barca a meno che un modo non sia altamente preferito a un altro.
Trevor Hickey,

4
@TrevorHickey: In realtà direi che ci sono due differenze chiave tra gli esempi che dai - (1) In uno alla funzione viene inizializzato un puntatore alla struttura, e nell'altro restituisce una struttura inizializzata; (2) In uno i parametri di inizializzazione vengono passati alla funzione e nell'altro sono impliciti. Sembra che tu stia chiedendo di più su (2), ma le risposte qui si stanno concentrando su (1). Potresti voler chiarire che - sospetto che la maggior parte delle persone raccomanderebbe un ibrido dei due usando parametri espliciti e un puntatore:void SetupFoo(Foo *out, int a, int b, int c)
psmears

1
In che modo il primo approccio porterebbe a una struttura "semi-inizializzata" Foo? Il primo approccio esegue anche tutte le inizializzazioni in un unico posto. (O stai considerando che una struttura non inizializzata Fooè "semi-inizializzata"?)
jamesdlin,

1
@jamesdlin nei casi in cui viene creato Foo e InitialiseFoo viene accidentalmente perso. Era solo una figura retorica per descrivere l'inizializzazione in 2 fasi senza scrivere una lunga descrizione. Ho pensato che il tipo di sviluppatore esperto avrebbe capito.
gbjbaanb,

22

Entrambi gli approcci raggruppano il codice di inizializzazione in una singola chiamata di funzione. Fin qui tutto bene.

Tuttavia, ci sono due problemi con il secondo approccio:

  1. Il secondo in realtà non costruisce l'oggetto risultante, inizializza un altro oggetto sullo stack, che viene quindi copiato sull'oggetto finale. Ecco perché vedrei il secondo approccio leggermente inferiore. Il push-back che hai ricevuto è probabilmente dovuto a questa copia estranea.

    Ciò è ancora peggio quando si ricava una classe Derivedda Foo(le strutture sono ampiamente utilizzate per l'orientamento degli oggetti in C): con il secondo approccio, la funzione ConstructDerived()chiamerebbe ConstructFoo(), copiando l' Foooggetto temporaneo risultante nello slot superclasse di un Derivedoggetto; completare l'inizializzazione Deriveddell'oggetto; solo per avere nuovamente l'oggetto risultante copiato nuovamente al ritorno. Aggiungi un terzo livello e l'intera cosa diventa completamente ridicola.

  2. Con il secondo approccio, le ConstructClass()funzioni non hanno accesso all'indirizzo dell'oggetto in costruzione. Ciò rende impossibile collegare oggetti durante la costruzione, poiché è necessario quando un oggetto deve registrarsi con un altro oggetto per un callback.


Infine, non tutte structssono classi a tutti gli effetti. Alcuni structseffettivamente raggruppano insieme un mucchio di variabili, senza alcuna restrizione interna ai valori di queste variabili. typedef struct Point { int x, y; } Point;sarebbe un buon esempio di questo. Per questi usi di una funzione di inizializzazione sembra eccessivo. In questi casi, la sintassi letterale composta può essere conveniente (è C99):

Point = { .x = 7, .y = 9 };

o

Point foo(...) {
    //other stuff

    return (Point){ .x = n, .y = n*n };
}

5
Non pensavo che le copie sarebbero state un problema a causa di en.wikipedia.org/wiki/Copy_elision
Trevor Hickey,

5
Il fatto che il compilatore possa eludere la copia non allevia il fatto che tu abbia scritto la copia. In C, scrivere operazioni superflue e fare affidamento sul compilatore per risolverle è considerato un cattivo stile. Ciò è diverso dal C ++ in cui le persone sono orgogliose quando possono dimostrare che il compilatore può teoricamente rimuovere tutta l'innesto lasciato dai loro modelli nidificati. In C, le persone cercano di scrivere esattamente il codice che la macchina dovrebbe eseguire. Comunque, il punto sugli indirizzi inaccessibili rimane, copia elisione non può aiutarti.
cmaster

3
Chiunque utilizzi un compilatore dovrebbe aspettarsi che il codice che scrive venga trasformato dal compilatore. A meno che non stiano eseguendo un interprete C hardware, il codice che scrivono non sarà il codice che eseguono, anche se è facile credere diversamente. Se capiscono il loro compilatore, capiranno l'elisione e non è diverso dal int x = 3;non memorizzare la stringax nel file binario. L'indirizzo e i punti di eredità sono buoni; il presunto fallimento dell'elisione è sciocco.
Yakk,

@Yakk: Storicamente, C è stato inventato per servire come una forma di linguaggio assembly di alto livello per la programmazione dei sistemi. Negli anni successivi, la sua identità è diventata sempre più oscura. Alcune persone vogliono che sia un linguaggio applicativo ottimizzato, ma poiché non è emersa una forma migliore di linguaggio assembly di alto livello, C è ancora necessario per svolgere quest'ultimo ruolo. Non vedo nulla di sbagliato nell'idea che un codice di programma ben scritto dovrebbe comportarsi almeno decentemente anche se compilato con ottimizzazioni minime, anche se per farlo funzionare davvero richiederebbe che C aggiunga alcune cose che mancava da tempo.
supercat

@Yakk: Avere direttive, ad esempio, che direbbero a un compilatore "Le seguenti variabili possono essere mantenute in modo sicuro nei registri durante il seguente tratto di codice", nonché un mezzo per copiare blocchi di un tipo diverso da quello unsigned charche consentirebbe ottimizzazioni per le quali il La rigida regola di aliasing sarebbe insufficiente, rendendo contemporaneamente più chiare le aspettative del programmatore.
supercat,

1

A seconda del contenuto della struttura e del particolare compilatore utilizzato, entrambi gli approcci potrebbero essere più veloci. Un modello tipico è che le strutture che soddisfano determinati criteri possono essere restituite nei registri; per le funzioni che restituiscono altri tipi di struttura, al chiamante è richiesto di allocare spazio per la struttura temporanea da qualche parte (in genere nello stack) e passare il suo indirizzo come parametro "nascosto"; nei casi in cui il ritorno di una funzione è memorizzato direttamente in una variabile locale il cui indirizzo non è detenuto da alcun codice esterno, alcuni compilatori potrebbero essere in grado di passare direttamente l'indirizzo di quella variabile.

Se un tipo di struttura soddisfa i requisiti di un'implementazione particolare da restituire nei registri (ad esempio, non essendo più grande di una parola macchina o riempiendo esattamente due parole macchina) con una funzione di ritorno, la struttura può essere più veloce del passaggio dell'indirizzo di una struttura, in particolare poiché esporre l'indirizzo di una variabile a un codice esterno che potrebbe conservarne una copia potrebbe precludere alcune utili ottimizzazioni. Se un tipo non soddisfa tali requisiti, il codice generato per una funzione che restituisce una struttura sarà simile a quello per una funzione che accetta un puntatore di destinazione; il codice chiamante sarebbe probabilmente più veloce per il modulo che utilizza un puntatore, ma il modulo perde alcune opportunità di ottimizzazione.

Peccato che C non fornisca un mezzo per dire che una funzione è vietata dal conservare una copia di un puntatore passato (semantica simile a un riferimento C ++) poiché passare un puntatore così limitato sarebbe ottenere i vantaggi diretti delle prestazioni del passaggio un puntatore a un oggetto preesistente, ma allo stesso tempo evitare i costi semantici di richiedere a un compilatore di considerare "esposto" l'indirizzo di una variabile.


3
All'ultimo punto: in C ++ non c'è nulla che impedisca a una funzione di conservare una copia di un puntatore che viene passato come riferimento, la funzione può semplicemente prendere l'indirizzo dell'oggetto. È anche libero di usare il riferimento per costruire un altro oggetto che contenga quel riferimento (nessun puntatore nudo creato). Tuttavia, la copia del puntatore o il riferimento nell'oggetto possono sopravvivere all'oggetto a cui puntano, creando un puntatore / riferimento pendente. Quindi il punto sulla sicurezza di riferimento è piuttosto muto.
cmaster

@cmaster: sulle piattaforme che restituiscono strutture passando un puntatore alla memoria temporanea, i compilatori C non forniscono funzioni chiamate con alcun modo di accedere all'indirizzo di tale memoria. In C ++, è possibile ottenere l'indirizzo di una variabile passata per riferimento, ma a meno che il chiamante non garantisca la durata dell'elemento passato (nel qual caso di solito avrebbe passato un puntatore) si otterrà probabilmente un comportamento indefinito.
supercat

1

Un argomento a favore dello stile "parametro di output" è che consente alla funzione di restituire un codice di errore.

struct MyStruct {
    int x;
    char *y;
    // ...
};

int MyStruct_init(struct MyStruct *out) {
    // ...
    char *c = malloc(n);
    if (!c) {
        return -1;
    }
    out->y = c;
    return 0;  // Success!
}

Considerando alcune serie di strutture correlate, se l'inizializzazione può fallire per ognuna di esse, può valere la pena farle usare tutte lo stile fuori parametro per motivi di coerenza.


1
Anche se uno potrebbe solo impostare errno.
Deduplicatore,

0

Presumo che il tuo focus sia sull'inizializzazione tramite parametro di output rispetto all'inizializzazione tramite return, non sulla discrepanza nel modo in cui vengono forniti gli argomenti di costruzione.

Si noti che il primo approccio potrebbe consentire Foodi essere opaco (sebbene non con il modo in cui lo si utilizza attualmente), e questo è generalmente desiderabile per la manutenibilità a lungo termine. Ad esempio, è possibile considerare una funzione che alloca una Foostruttura opaca senza inizializzarla. O forse è necessario inizializzare nuovamente una Foostruttura che era stata inizializzata in precedenza con valori diversi.


Downvoter, ti interessa spiegare? Qualcosa che ho detto è effettivamente errato?
jamesdlin,
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.