È meglio in C ++ passare per valore o passare per riferimento costante?


Risposte:


203

Viene prodotto generalmente raccomandato migliori pratiche 1 per uso passaggio da ref const per tutti i tipi , eccetto incorporato tipi ( char, int, double, etc.), per iteratori e oggetti funzione (lambda, classi derivanti da std::*_function).

Ciò era particolarmente vero prima dell'esistenza della semantica di mosse . Il motivo è semplice: se si passa per valore, è necessario creare una copia dell'oggetto e, ad eccezione di oggetti molto piccoli, è sempre più costoso che passare un riferimento.

Con C ++ 11 abbiamo acquisito la semantica dei movimenti . In poche parole, spostare la semantica consente che, in alcuni casi, un oggetto possa essere passato "per valore" senza copiarlo. In particolare, questo è il caso in cui l'oggetto che si sta passando è un valore .

Di per sé, spostare un oggetto è ancora costoso almeno quanto passare per riferimento. Tuttavia, in molti casi una funzione copierà comunque internamente un oggetto, ovvero assumerà la proprietà dell'argomento. 2

In queste situazioni abbiamo il seguente compromesso (semplificato):

  1. Possiamo passare l'oggetto per riferimento, quindi copiarlo internamente.
  2. Possiamo passare l'oggetto per valore.

"Passa per valore" fa comunque copiare l'oggetto, a meno che l'oggetto non sia un valore. Nel caso di un valore, l'oggetto può invece essere spostato, in modo che il secondo caso non sia più improvvisamente "copia, quindi sposta" ma "sposta, quindi (potenzialmente) sposta di nuovo".

Per oggetti di grandi dimensioni che implementano costruttori di mosse adeguati (come vettori, stringhe ...), il secondo caso è quindi molto più efficiente del primo. Pertanto, si consiglia di utilizzare il passaggio per valore se la funzione diventa proprietaria dell'argomento e se il tipo di oggetto supporta uno spostamento efficiente .


Una nota storica:

In effetti, qualsiasi compilatore moderno dovrebbe essere in grado di capire quando passare per valore è costoso e, se possibile, convertire implicitamente la chiamata per utilizzare un riferimento const.

In teoria.In pratica, i compilatori non possono sempre cambiarlo senza interrompere l'interfaccia binaria della funzione. In alcuni casi speciali (quando la funzione è incorporata) la copia verrà effettivamente elusa se il compilatore riesce a capire che l'oggetto originale non verrà modificato attraverso le azioni nella funzione.

Ma in generale il compilatore non può determinarlo, e l'avvento della semantica di spostamento in C ++ ha reso questa ottimizzazione molto meno rilevante.


1 Ad esempio in Scott Meyers, C ++ efficace .

2 Questo è particolarmente vero per i costruttori di oggetti, che possono prendere argomenti e archiviarli internamente per far parte dello stato dell'oggetto costruito.


hmmm ... Non sono sicuro che valga la pena passare per rif. double-s
sergtk,

3
Come al solito, boost aiuta qui. boost.org/doc/libs/1_37_0/libs/utility/call_traits.htm ha elementi di template per capire automaticamente quando un tipo è un tipo incorporato (utile per i template, dove a volte non si può sapere così facilmente).
CesarB,

13
Questa risposta manca un punto importante. Per evitare il taglio, è necessario passare per riferimento (const o altro). Vedere stackoverflow.com/questions/274626/...
ChrisN

6
@ Chris: giusto. Ho lasciato fuori tutta la parte del polimorfismo perché questa è una semantica completamente diversa. Credo che l'OP (semanticamente) abbia significato il passaggio dell'argomento "per valore". Quando sono richieste altre semantiche, la domanda non si pone nemmeno.
Konrad Rudolph,

98

Modifica: nuovo articolo di Dave Abrahams su cpp-next:

Vuoi velocità? Passa per valore.


Passare per valore per le strutture in cui la copia è economica ha l'ulteriore vantaggio che il compilatore può presumere che gli oggetti non siano alias (non sono gli stessi oggetti). Utilizzando il pass-by-reference il compilatore non può presumere che sempre. Esempio semplice:

foo * f;

void bar(foo g) {
    g.i = 10;
    f->i = 2;
    g.i += 5;
}

il compilatore può ottimizzarlo in

g.i = 15;
f->i = 2;

poiché sa che feg non condividono la stessa posizione. se g fosse un riferimento (pippo &), il compilatore non avrebbe potuto assumerlo. poiché gi potrebbe quindi essere aliasato da f-> i e deve avere un valore di 7. quindi il compilatore dovrebbe recuperare nuovamente il nuovo valore di gi dalla memoria.

Per regole più pratiche, ecco un buon insieme di regole che si trovano nell'articolo Move Constructors (lettura altamente consigliata).

  • Se la funzione intende modificare l'argomento come effetto collaterale, prendilo per riferimento non const.
  • Se la funzione non modifica il suo argomento e l'argomento è di tipo primitivo, prendilo per valore.
  • Altrimenti prendilo per riferimento const, tranne nei seguenti casi
    • Se la funzione avrebbe quindi bisogno di fare comunque una copia del riferimento const, prendilo per valore.

"Primitivo" sopra significa tipi di dati sostanzialmente piccoli che sono lunghi pochi byte e non sono polimorfici (iteratori, oggetti funzione, ecc ...) o costosi da copiare. In quel documento, c'è un'altra regola. L'idea è che a volte si vuole fare una copia (nel caso in cui l'argomento non possa essere modificato), e a volte non si vuole (nel caso in cui si desideri utilizzare l'argomento stesso nella funzione se l'argomento era comunque temporaneo , per esempio). L'articolo spiega in dettaglio come è possibile farlo. In C ++ 1x tale tecnica può essere utilizzata in modo nativo con il supporto del linguaggio. Fino ad allora, andrei con le regole di cui sopra.

Esempi: per rendere una stringa maiuscola e restituire la versione maiuscola, si dovrebbe sempre passare per valore: si deve comunque prenderne una copia (non si può cambiare direttamente il riferimento const) - quindi meglio renderlo il più trasparente possibile per il chiamante e crea quella copia in anticipo in modo che il chiamante possa ottimizzare il più possibile - come dettagliato in quel documento:

my::string uppercase(my::string s) { /* change s and return it */ }

Tuttavia, se non è necessario modificare comunque il parametro, prenderlo facendo riferimento a const:

bool all_uppercase(my::string const& s) { 
    /* check to see whether any character is uppercase */
}

Tuttavia, se lo scopo del parametro è scrivere qualcosa nell'argomento, passarlo per riferimento non const

bool try_parse(T text, my::string &out) {
    /* try to parse, write result into out */
}

ho trovato buone le tue regole, ma non sono sicuro della prima parte in cui parli di non passarle perché un ref lo accelererebbe. sì certo, ma non passare qualcosa come riferimento solo per l'ottimizzazione non ha alcun senso. se si desidera modificare l'oggetto stack in cui si sta passando, farlo rif. se non lo fai, passalo per valore. se non vuoi cambiarlo, passalo come const-ref. l'ottimizzazione fornita con il pass-by-value non dovrebbe avere importanza poiché si ottengono altre cose passando come riferimento. non capisco il "vuoi velocità?" sice se dove eseguirai quelle operazioni
passeresti

Johannes: Ho adorato quell'articolo quando l'ho letto, ma sono rimasto deluso quando l'ho provato. Questo codice non è riuscito su GCC e MSVC. Ho perso qualcosa o non funziona in pratica?
user541686

Non credo di essere d'accordo sul fatto che se vuoi fare una copia comunque, la passeresti per valore (invece di const ref), quindi spostarla. Guardalo in questo modo, cosa c'è di più efficiente, una copia e una mossa (puoi anche averne 2 copie se la passi in avanti) o solo una copia? Sì, ci sono alcuni casi speciali su entrambi i lati, ma se i tuoi dati non possono comunque essere spostati (es: un POD con tonnellate di numeri interi), non sono necessarie copie extra.
Ion Todirel,

2
Mehrdad, non so cosa vi aspettavate, ma i lavori di codice come previsto
Ion Todirel

Considererei la necessità di copiare solo per convincere il compilatore che i tipi non si sovrappongono a una carenza del linguaggio. Preferirei usare GCC __restrict__(che può anche funzionare su riferimenti) piuttosto che fare copie eccessive. Peccato che lo standard C ++ non abbia adottato la restrictparola chiave C99 .
Ruslan,

12

Dipende dal tipo. Stai aggiungendo il piccolo sovraccarico di dover fare un riferimento e una dereferenza. Per i tipi con dimensioni uguali o inferiori ai puntatori che utilizzano il ctor di copia predefinito, sarebbe probabilmente più veloce passare per valore.


Per i tipi non nativi, potresti (a seconda di quanto bene il compilatore ottimizza il codice) ottenere un aumento delle prestazioni usando i riferimenti const anziché solo i riferimenti.
GU.

9

Come è stato sottolineato, dipende dal tipo. Per i tipi di dati integrati, è meglio passare per valore. Anche alcune strutture molto piccole, come una coppia di ints, possono funzionare meglio passando per valore.

Ecco un esempio, supponiamo che tu abbia un valore intero e desideri passarlo a un'altra routine. Se quel valore è stato ottimizzato per essere archiviato in un registro, quindi se si desidera passarlo come riferimento, deve prima essere memorizzato e quindi un puntatore a quella memoria posizionata nello stack per eseguire la chiamata. Se veniva passato per valore, tutto ciò che è richiesto è il registro inserito nello stack. (I dettagli sono un po 'più complicati di quelli dati da diversi sistemi di chiamata e CPU).

Se stai programmando un modello, di solito sei costretto a passare sempre da const ref poiché non conosci i tipi che vengono passati. Passare le penalità per aver passato qualcosa di brutto in base al valore è molto peggio delle penalità del passare un tipo incorporato di const ref.


Nota sulla terminologia: una struttura contenente un milione di ints è ancora un "tipo POD". Forse intendi "per i tipi predefiniti è meglio passare per valore".
Steve Jessop,

6

Questo è ciò su cui lavoro normalmente durante la progettazione dell'interfaccia di una funzione non modello:

  1. Passa per valore se la funzione non desidera modificare il parametro e il valore è economico da copiare (int, double, float, char, bool, ecc ... Notare che std :: string, std :: vector e il resto dei contenitori nella libreria standard NON sono)

  2. Passa dal puntatore const se il valore è costoso da copiare e la funzione non vuole modificare il valore indicato e NULL è un valore gestito dalla funzione.

  3. Passa da un puntatore non const se il valore è costoso da copiare e la funzione vuole modificare il valore indicato e NULL è un valore che la funzione gestisce.

  4. Passa per riferimento const quando il valore è costoso da copiare e la funzione non vuole modificare il valore a cui fa riferimento e NULL non sarebbe un valore valido se invece fosse utilizzato un puntatore.

  5. Passa per riferimento non const quando il valore è costoso da copiare e la funzione vuole modificare il valore a cui fa riferimento e NULL non sarebbe un valore valido se invece fosse usato un puntatore.


Aggiungi std::optionalall'immagine e non hai più bisogno di puntatori.
Violet Giraffe

5

Sembra che tu abbia la tua risposta. Il passaggio per valore è costoso, ma ti dà una copia con cui lavorare se ne hai bisogno.


Non sono sicuro del motivo per cui questo è stato votato giù? Ha senso per me. Se hai bisogno del valore attualmente memorizzato, passa per valore. In caso contrario, passare il riferimento.
Totty

4
Dipende totalmente dal tipo. Fare un tipo POD (semplici vecchi dati) per riferimento può infatti ridurre le prestazioni causando più accessi alla memoria.
Torlack,

1
Ovviamente passare int per riferimento non salva nulla! Penso che la domanda implichi cose che sono più grandi di un puntatore.
GeekyMonkey,

4
Non è così ovvio, ho visto un sacco di codice da parte di persone che non capiscono veramente come funzionano i computer passando cose semplici da const ref perché è stato detto loro che è la cosa migliore da fare.
Torlack,

4

Di norma è meglio passare per riferimento const. Ma se è necessario modificare l'argomento della funzione localmente, è meglio utilizzare il passaggio per valore. Per alcuni tipi di base le prestazioni in generale sono le stesse sia per il passaggio per valore che per riferimento. In realtà riferimento internamente rappresentato dal puntatore, ecco perché ci si può aspettare, ad esempio, che per il puntatore sia il passaggio sia lo stesso in termini di prestazioni, o anche il passaggio in base al valore può essere più veloce a causa della inutile dereferenza.


Se è necessario modificare la copia del parametro della chiamata, è possibile effettuare una copia nel codice chiamato anziché passare per valore. In genere non dovresti scegliere l'API in base a un dettaglio dell'implementazione del genere: l'origine del codice chiamante è la stessa in entrambi i modi, ma il suo codice oggetto non lo è.
Steve Jessop,

Se si passa per valore, viene creata una copia. E IMO non importa in che modo creare una copia: tramite argomento che passa per valore o localmente - questo è ciò che riguarda il C ++. Ma dal punto di vista del design sono d'accordo con te. Ma descrivo solo qui le funzionalità C ++ e non tocco il design.
sergtk,

1

Come regola generale, valore per i tipi non di classe e riferimento const per le classi. Se una classe è veramente piccola, probabilmente è meglio passare per valore, ma la differenza è minima. Quello che vuoi davvero evitare è passare una classe gigantesca in base al valore e averlo tutto duplicato - questo farà un'enorme differenza se stai passando, diciamo, uno std :: vector con alcuni elementi al suo interno.


La mia comprensione è che std::vectoreffettivamente alloca i suoi elementi sull'heap e l'oggetto vettoriale stesso non cresce mai. Oh aspetta. Se l'operazione provoca la creazione di una copia del vettore, in realtà andrà a duplicare tutti gli elementi. Sarebbe male.
Steven Lu

1
Sì, è quello che stavo pensando. sizeof(std::vector<int>)è costante, ma passarlo per valore copierà comunque il contenuto in assenza di intelligenza del compilatore.
Peter,

1

Passa per valore per piccoli tipi.

Passa per i riferimenti const per i grandi tipi (la definizione di big può variare tra le macchine) MA, in C ++ 11, passa per valore se stai per consumare i dati, poiché puoi sfruttare la semantica di spostamento. Per esempio:

class Person {
 public:
  Person(std::string name) : name_(std::move(name)) {}
 private:
  std::string name_;
};

Ora il codice chiamante farebbe:

Person p(std::string("Albert"));

E solo un oggetto verrebbe creato e spostato direttamente nel membro name_della classe Person. Se si passa per riferimento const, sarà necessario crearne una copia per inserirla name_.


-5

Differenza semplice: - Nella funzione abbiamo un parametro di input e output, quindi se il tuo parametro di input e output di passaggio è lo stesso, usa call by reference altrimenti se i parametri di input e output sono diversi, è meglio usare call per value.

esempio void amount(int account , int deposit , int total )

parametro di input: conto, parametro di output del deposito: totale

input e out è diverso usa call by vaule

  1. void amount(int total , int deposit )

input totale deposito output totale

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.