2010-01-26 20 views
8

Ho visto numerosi argomenti che l'utilizzo di un valore di ritorno è preferibile ai parametri. Sono convinto dei motivi per cui evitarli, ma non sono sicuro di trovarmi nei casi in cui è inevitabile.Come evitare i parametri?

Part One della mia domanda è: Quali sono alcuni dei tuoi modi preferiti/comuni di spostarsi usando un parametro out? Stuff lungo le linee: uomo, nelle recensioni tra colleghi vedo sempre gli altri programmatori farlo quando avrebbero potuto farlo facilmente in questo modo.

Seconda parte della mia domanda riguarda alcuni casi specifici che ho riscontrato in cui vorrei evitare un parametro esterno ma non riesco a pensare a un modo pulito per farlo.

Esempio 1: Ho una classe con una copia costosa che vorrei evitare. Il lavoro può essere fatto sull'oggetto e questo crea l'oggetto per essere costoso da copiare. Il lavoro per costruire i dati non è esattamente banale. Attualmente, passerò questo oggetto in una funzione che modificherà lo stato dell'oggetto. Questo per me è preferibile a new'ing l'oggetto interno alla funzione worker e restituirlo indietro, in quanto mi permette di tenere le cose in pila.

class ExpensiveCopy //Defines some interface I can't change. 
{ 
public: 
    ExpensiveCopy(const ExpensiveCopy toCopy){ /*Ouch! This hurts.*/ }; 
    ExpensiveCopy& operator=(const ExpensiveCopy& toCopy){/*Ouch! This hurts.*/}; 

    void addToData(SomeData); 
    SomeData getData(); 
} 

class B 
{ 
public: 
    static void doWork(ExpensiveCopy& ec_out, int someParam); 
    //or 
    // Your Function Here. 
} 

Utilizzando la mia funzione, vengo chiamando codice come questo:

const int SOME_PARAM = 5; 
ExpensiveCopy toModify; 
B::doWork(toModify, SOME_PARAM); 

mi piacerebbe avere qualcosa di simile:

ExpensiveCopy theResult = B::doWork(SOME_PARAM); 

Ma io non so se questo è possibile.

Secondo esempio: Ho una matrice di oggetti. Gli oggetti nell'array sono di tipo complesso e ho bisogno di lavorare su ogni elemento, lavoro che mi piacerebbe mantenere separato dal ciclo principale che accede a ciascun elemento. Il codice al momento è simile al seguente:

std::vector<ComplexType> theCollection; 
for(int index = 0; index < theCollection.size(); ++index) 
{ 
    doWork(theCollection[index]); 
} 

void doWork(ComplexType& ct_out) 
{ 
    //Do work on the individual element. 
} 

Qualche suggerimento su come gestire alcune di queste situazioni? Io lavoro principalmente in C++, ma sono interessato a vedere se altre lingue facilitano una configurazione più semplice. Ho incontrato RVO come una possibile soluzione, ma ho bisogno di leggere di più su di esso e suona come una caratteristica specifica del compilatore.

+0

La semantica di un parametro di riferimento non const è che verrà (può) essere modificata nella funzione. Se non avessi intenzione di cambiarlo, lo passeresti come riferimento const, dopotutto. – Bill

+0

* "ma sono interessato a vedere se altre lingue facilitano una configurazione più semplice" * - linguaggi come Java e C# semplicemente non hanno questo problema dato che passano sempre i riferimenti agli oggetti in giro. –

+0

Meravigliose risposte da parte di tutti. Ho avuto la sensazione che nei miei esempi specifici non ci sarebbe stata molta altra opzione.Ho visto post su SO da persone che sono così irremovibili su come evitare i parametri a tutti i costi. Speravo che forse uno di loro potesse approfondire il modo in cui ottengono una tale impresa. Per quanto riguarda RVO, sono un po 'titubante nell'usare caratteristiche specifiche del compilatore, dato che sarebbe difficile spiegare ai miei capi perché devo ridisegnare metà del mio codice perché non funziona più a causa di una caratteristica mancante come risultato di un modifica/aggiornamento del compilatore. –

risposta

7

Non sono sicuro del motivo per cui stai cercando di evitare di passare i riferimenti qui. Sono praticamente queste situazioni in cui esiste una semantica pass-by-reference.

Il codice

static void doWork(ExpensiveCopy& ec_out, int someParam); 

sembra perfettamente bene per me.

Se davvero si vuole modificarlo, allora hai un paio di opzioni

  1. Spostare DoWork modo che è si tratta di un membro della ExpensiveCopy (che si dice non si può fare, in modo che è fuori)
  2. restituisce un puntatore (intelligente) da doWork invece di copiarlo.(Che non si vuole fare come si vuole mantenere le cose nello stack)
  3. contare su RVO (che altri hanno fatto notare è supportato da praticamente tutti i compilatori moderni)
+0

concordato. Solo una cosa, l'opzione 1 che hai menzionato non è disponibile, poiché l'OP dice che 'ExpensiveCopy' non può essere modificato. – stakx

+0

@stakx, hai ragione, mi sono perso. Risolverà che – Glen

+0

@stakx: so che l'OP ha detto che non può modificare la classe. Ma perché non può creare una sottoclasse e aggiungere il metodo doWork() lì? – slebetman

1

A meno che non si sta andando lungo il percorso "tutto è immutabile", che non si adatta troppo bene al C++. non è possibile evitare facilmente i parametri. La libreria standard C++ li usa e ciò che è abbastanza buono per me è abbastanza buono per me.

+0

Sono totalmente in disaccordo con l'ultima affermazione: sfortunatamente la libreria standard C++ non è vicino all'ideale (solo in cima alla mia testa: std :: string follia dei membri, flussi troppo elaborati, ecc.) Non voglio iniziare holiwar ma io Immagino che non sia una buona idea giustificare qualcosa a causa di un esempio ben noto. –

+2

Ovviamente ha le sue verruche, ma in generale lo trovo estremamente potente e facile da usare - desidero solo che il mio codice sia ben progettato. –

0

Come per il primo esempio: return value optimization consente spesso di creare l'oggetto restituito direttamente sul posto, invece di dover copiare l'oggetto. Tutti i compilatori moderni fanno questo.

3

Ogni compilatore utile fa (ottimizzazione valore di ritorno) RVO se le ottimizzazioni sono abilitati, pertanto la seguente modo efficace non comporta la copia:

Expensive work() { 
    // ... no branched returns here 
    return Expensive(foo); 
} 

Expensive e = work(); 

In compilatori possono applicare NRVO alcuni casi, l'ottimizzazione valore di ritorno di nome, pure:

Expensive work() { 
    Expensive e; // named object 
    // ... no branched returns here 
    return e; // return named object 
} 

Questo tuttavia non è esattamente affidabile, funziona solo in casi più banali e dovrebbe essere testato. Se non hai intenzione di testare ogni caso, usa solo i parametri out con i riferimenti nel secondo caso.

2

IMO la prima cosa che dovresti chiederti è se copiare ExpensiveCopy è davvero così proibitivo costoso. E per rispondere a questo, di solito avrai bisogno di un profiler. A meno che un profiler non ti dica che la copia è davvero un collo di bottiglia, scrivi semplicemente il codice che è più facile da leggere: ExpensiveCopy obj = doWork(param);.

Naturalmente, ci sono effettivamente casi in cui gli oggetti non possono essere copiati per le prestazioni o per altri motivi. Quindi si applica Neil's answer.

0

Su quale piattaforma stai lavorando?

La ragione per cui lo chiedo è che molte persone hanno suggerito l'ottimizzazione del valore di ritorno, che è un molto pratico complemento dell'ottimizzazione presente in quasi tutti i compilatori. Inoltre, Microsoft e Intel implementano ciò che chiamano Ottimizzazione dei valori di ritorno nominali, che è ancora più utile.

In Standard Return Value Optimization l'istruzione return è una chiamata al costruttore di un oggetto, che indica al compilatore di eliminare i valori temporanei (non necessariamente l'operazione di copia).

In Named Return Value Optimization è possibile restituire un valore con il suo nome e il compilatore farà la stessa cosa. Il vantaggio di NRVO è che è possibile eseguire operazioni più complesse sul valore creato (come richiamare funzioni su di esso) prima di restituirlo.

Mentre nessuno di questi elimina veramente una copia costosa se i dati restituiti sono molto grandi, aiutano.

In termini di evitare la copia, l'unico vero modo per farlo è con puntatori o riferimenti perché la tua funzione deve modificare i dati nel luogo in cui vuoi che finisca. Ciò significa che probabilmente vuoi avere un parametro di riferimento pass-by.

Inoltre, devo precisare che il pass-by-reference è molto comune nel codice ad alte prestazioni proprio per questo motivo. La copia dei dati può essere incredibilmente costosa, ed è spesso qualcosa che le persone trascurano quando ottimizzano il loro codice.

2

In aggiunta a tutti i commenti qui mi piacerebbe ricordare che in C++ 0x raramente usereste parametro di uscita a scopo di ottimizzazione - a causa dei Costruttori Move (vedi here)

+0

In realtà ho provato di recente a configurare un sistema per il supporto delle letture di file che utilizzavano una volta la semantica del movimento. Ha funzionato alla grande, ma alla fine sono stato bruciato a causa di incompatibilità con diversi compilatori. Ho dovuto cambiare tutto in parametri che era un vero peccato. –

0

Per quanto posso vedere , i motivi per preferire i valori di ritorno ai parametri out sono che è più chiaro e funziona con la pura programmazione funzionale (puoi ottenere delle buone garanzie se una funzione dipende solo dai parametri di input, restituisce un valore e non ha effetti collaterali). Il primo motivo è stilistico, e secondo me non è poi così importante. Il secondo non si adatta bene al C++. Pertanto, non proverei a distorcere nulla per evitare i parametri.

Il semplice fatto è che alcune funzioni devono restituire più cose, e nella maggior parte delle lingue questo suggerisce i parametri. Common Lisp ha multiple-value-bind e multiple-value-return, in cui un elenco di simboli viene fornito dal binding e viene restituito un elenco di valori. In alcuni casi, una funzione può restituire un valore composito, come un elenco di valori che verranno quindi decostruiti e non è un grosso problema per una funzione C++ restituire un valore std::pair. Restituire più di due valori in questo modo in C++ diventa complicato. È sempre possibile definire una struttura, ma definirla e crearla sarà spesso più complicata dei parametri.

In alcuni casi, il valore di ritorno si sovraccarica. In C, getchar() restituisce un int, con l'idea che ci sono più valori int che char (vero in tutte le implementazioni che conosco, falso in alcuni che posso facilmente immaginare), quindi uno dei valori può essere usato per indicare end- del file. atoi() restituisce un numero intero, o il numero intero rappresentato dalla stringa passata o zero se non ce n'è, quindi restituisce la stessa cosa per "0" e "rana". (Se vuoi sapere se c'era un valore int o no, usa strtol(), che ha un parametro out.)

C'è sempre la tecnica di lanciare un'eccezione in caso di errore, ma non tutti i valori di ritorno multipli sono errori, e non tutti gli errori sono eccezionali.

Quindi, i valori di ritorno sovraccarico causano problemi, i ritorni a più valori non sono facili da utilizzare in tutte le lingue e non esistono sempre singoli ritorni. Lanciare un'eccezione è spesso inappropriata. L'utilizzo di parametri è molto spesso la soluzione più pulita.

0

Chiediti perché hai un metodo che esegue il lavoro su questo costoso oggetto da copiare in primo luogo. Diciamo che hai un albero, manderesti l'albero in qualche metodo di costruzione o darei l'albero come metodo di costruzione? Situazioni come questa si presentano costantemente quando si ha un po 'di design, ma tendono a ripiegarsi su se stessi quando ce l'hai.

So che per praticità non è sempre possibile modificare ogni oggetto, ma i parametri di passaggio in entrata sono un'operazione di effetto collaterale, e rende molto più difficile capire cosa sta succedendo, e non si è mai veramente per farlo (tranne come forzato lavorando all'interno di quadri di codice altrui).

A volte è più facile, ma non è assolutamente consigliabile utilizzarlo senza alcun motivo (se hai sofferto di alcuni progetti di grandi dimensioni in cui ci sono sempre una mezza dozzina di parametri, capirai cosa intendo).

Problemi correlati