Quando una funzione deve assumere un puntatore per una raccolta da riempire e restituire un puntatore con una raccolta piena?

3

In C ++ vedo spesso queste due firme usate in modo apparentemente intercambiabile:

void fill_array(Array<Type>* array_to_fill);
Array<Type>* filled_array();

Immagino ci sia una sottile differenza, ma non so cosa sia. Qualcuno potrebbe spiegare quando potrei preferire un modulo all'altro?

    
posta calben 22.04.2016 - 21:16
fonte

4 risposte

8

Il primo tipo di firma è solitamente preferibile.

La differenza è che la seconda firma richiede che l'array venga creato all'interno della funzione. In particolare, la seconda firma richiede in effetti che la matrice sopravviva all'ambito in cui è stata creata . Quindi, quello che stiamo realmente confrontando sono questi due frammenti:

function foo1() {
    Array<Type>* array = /* allocate memory and call constructors */
    fill_array(array);
    do_stuff_with_array(array);
    /* free memory and call destructors */
}

function foo2() {
    Array<Type>* array = filled_array(); /* allocation/constructors happen elsewhere */
    do_stuff_with_array(array);
    /* free memory and call destructors */
}

La seconda versione è potenzialmente problematica per alcuni motivi:

  1. È incline agli errori. Le funzioni che restituiscono puntatori o riferimenti a qualcosa che hanno creato sono molto facili da sbagliare, sia sotto forma di comportamento indefinito o sotto forma di prestazioni completamente inutili perdita. Dato che stai lavorando con i puntatori raw, è facile invocare un comportamento non definito restituendo un puntatore a una variabile locale che non è più valida dopo che la funzione è tornata. Se la matrice viene passata come oggetto normale o come riferimento, potresti subire una copia costosa quando filled_array() restituisce (i dettagli di quando questo può o non può accadere sono complicati, vedi StackOverflow per tutti i dettagli cruenti).

  2. Non sai come filled_array() ha allocato la memoria per l'array , quindi in linea di principio non sai come deallocarlo correttamente. Potresti riuscire a farla finta di presupporre che sia stata assegnata "normalmente", ma se non controlli personalmente l'allocazione, non lo sai per certo. È possibile che sia stato utilizzato un allocatore personalizzato ed è anche possibile che il puntatore sia stato salvato da qualche parte in modo che una funzione totalmente diversa possa eseguire la deallocazione in un momento specifico successivo (credo sia comune nelle librerie C). Mentre una funzione che accetta un puntatore come argomento potrebbe teoricamente farlo, è molto meno probabile.

  3. Riutilizzo memoria / oggetto. Che succede se ho già una memoria allocata per una matrice o una matrice effettiva, quando arriva il momento di chiamare filled_array() ? Sfortunatamente, filled_array() controlla sia l'allocazione di memoria sia la logica di generazione del valore, quindi assegnerà più memoria indipendentemente dal fatto che tu ne abbia bisogno. Se hai molte funzioni come questa in una riga, stai potenzialmente sprecando un'enorme quantità di tempo e memoria su allocazioni che potrebbero essere completamente ignorate se invece accettassi puntatori o riferimenti alla memoria controllata dal codice client. O in modo più conciso: Evita di scrivere funzioni che decidano come la memoria dovrebbe essere gestita e fare qualcos'altro con quella stessa memoria. Principio della responsabilità unica e tutto il resto.

Ovviamente, dovresti passare la matrice attorno per riferimento anziché per puntatore. E dovresti usare il più possibile gli oggetti RAII (che significano "solo un array" o un puntatore intelligente per un array) in modo che tutta l'allocazione e la deallocazione siano gestite per te. Ma questi argomenti per creare l'oggetto con l'ambito corretto si applicano ancora, poiché il passaggio ai riferimenti e agli oggetti RAII da solo può solo cambiare bug di correttezza in "bug" delle prestazioni (alcuni dei quali non possono essere sistemati automaticamente dalla semantica).

    
risposta data 22.04.2016 - 21:56
fonte
3

La risposta corretta è:

C. None of the above.

Opzione A:

void fill_array(Array<Type>* array_to_fill);

Questo è più idiomatico per il codice pre-C ++ 11 in cui i puntatori intelligenti erano fastidiosi a causa della mancanza di semantica del movimento e continua a essere il più sicuro delle due opzioni.

Il tasto qui è che la funzione non "possiede" la memoria: esegue un compito e un solo compito. Riempie semplicemente la matrice. La proprietà va oltre lo scopo di ciò che fa ed è per lo più sicura.

Opzione B:

Array<Type>* filled_array();

Questo crea un oggetto all'interno della funzione. Ci sono generalmente due modi per farlo:

  1. Restituisce un puntatore a un oggetto locale (stack). Questo puntatore non sarà valido a lungo e interromperà qualcosa (UB).
  2. Restituisce un puntatore a un oggetto (heap) assegnato dinamicamente. Chi possiede l'oggetto? Dove e quando viene liberata la sua memoria? In alcune architetture questo può porre ulteriori problemi. Ad esempio, la piattaforma Windows non consente la liberazione della memoria allocata in un modulo in un'altra (cioè non può allocare nella DLL 1 e libera nella DLL 2).

Opzione C:

 std::unique_ptr<Array<Type>> filled_array();

Questo è il modo assolutamente corretto, efficiente, garantito per lavorare.

La memoria è di proprietà del puntatore intelligente e verrà liberata quando il puntatore esce dal campo di applicazione (RAII) e non ha altri puntatori a cui consegnarlo.

All'interno della funzione, ci sarà un puntatore intelligente assegnato a una nuova istanza di array. Il puntatore intelligente possiede quel puntatore di matrice.

La funzione richiama quindi il comportamento attraverso quel puntatore, aggiungendo dati.

Quando la funzione è terminata, restituisce il puntatore intelligente. Poiché il puntatore intelligente esce dall'ambito, si distrugge. Tuttavia, sposta il puntatore che contiene in una nuova istanza nel percorso di chiamata. (Nota: il compilatore potrebbe essere in grado di costruire il puntatore intelligente sul posto nella posizione di chiamata.Indipendentemente, i puntatori intelligenti sono estremamente leggeri e la differenza è minima).

Ora hai un puntatore intelligente nella posizione di chiamata che possiede l'array a cui punta, e quell'array verrà liberato in base alle regole RAII standard. Non ci sono puntatori penzolanti, nessuna perdita di memoria, solo il codice che fa esattamente quello che sembra.

    
risposta data 27.04.2016 - 19:54
fonte
2

Sembra molto probabile che il secondo restituisca Array<Type> e non Array<Type>* . Nel primo caso, c'è un Array<Type> da qualche parte e si passa un puntatore ad esso, quindi la funzione può riempirlo. Nel secondo caso, la funzione crea un oggetto e lo restituisce (a meno che il tipo non sia Array<Type>* e non so cosa sta succedendo).

Se si usano i costruttori con riferimento a rvalue, il secondo è altrettanto efficiente ma molto più leggibile. Senza riferimenti rvalue (versioni precedenti di C ++), la prima versione era molto più efficiente, poiché la seconda necessità di creare una copia completamente inutile del valore restituito.

Chiarimento: presumo che la seconda funzione restituisca una matrice, non un puntatore a una matrice. Restituire un puntatore a un array significherebbe che ci deve essere una matrice da qualche parte. Allora, dov'è? Non ha senso. Ma se la funzione restituisce una matrice, tale matrice verrebbe probabilmente creata come variabile locale all'interno della funzione. Quindi un costruttore di assegnazioni viene chiamato a copia su un oggetto all'interno della funzione chiamante e l'originale viene distrutto. E c'è la copia che puoi evitare nelle nuove versioni di C ++.

    
risposta data 22.04.2016 - 21:24
fonte
2

Hai già una risposta accettata, ma ne aggiungo una nuova (perché non sono d'accordo con ciò che ha detto @ixrec).

I imagine there is a subtle difference, but I don't know what it is. Could someone explain when I might prefer one form over the other?

Idealmente (in un mondo perfetto), dovresti usare il secondo modulo, per tre ragioni:

  • compone
  • utilizza naturalmente il valore restituito per restituisce il valore che la funzione calcola
  • è più idioma scrivere codice client

Esempio, per queste funzioni (nota: le ho spostate in riferimento e valore invece che in puntatori):

void fill_array(Array<Type>& array_to_fill);
Array<Type> filled_array();
void use_array(const Array<Type>& x); // this is the client code

// first call
Array<Type> a;
fill_array(a);
use_array(a);

// second call (more idiomatic)
use_array( fill_array() );

Nel mondo reale (al contrario di "Idealmente"), la seconda forma può avere alcuni problemi:

  • potrebbe essere che restituire l'array creato è proibitivo (ad esempio, per oggetti che non supportano lo spostamento)
  • potrebbe essere che (anche con lo spostamento) il ritorno di valore può essere costoso (si consideri lo spostamento di un array estremamente grande)
  • non consente la trasmissione di informazioni di errore, in basi di codice che non supportano eccezioni:

    In tale code base (che non supporta eccezioni), la prima forma della funzione non restituisce nulla, ma un codice di errore. Con il secondo modulo, il codice di errore non può essere restituito (quindi la seconda forma tende ad essere inaccettabile se non puoi lanciare dalla funzione).

Se il tuo codice non soddisfa nessuna di queste restrizioni, usa il primo modulo. Se ha queste restrizioni (nessuna eccezione, costoso per passare la matrice in giro, o necessità di riempire sempre una matrice esistente) usa il secondo modulo.

È anche una soluzione per implementare entrambe le funzioni, una in termini di altre.

    
risposta data 26.04.2016 - 16:24
fonte

Leggi altre domande sui tag