TL; DR: passare per riferimento const è ancora una buona idea in C ++, tutto sommato. Non un'ottimizzazione prematura.
TL; DR2: la maggior parte degli adagi non ha senso, finché non lo fanno.
Scopo
Questa risposta cerca solo di estendere un po ' l'elemento collegato nelle Linee guida di base C ++ (menzionato per la prima volta nel commento di Amon).
Questa risposta non cerca di affrontare il problema di come pensare e applicare correttamente i vari adagi che erano ampiamente diffusi all'interno dei circoli dei programmatori, in particolare il problema della riconciliazione tra conclusioni o prove contrastanti.
applicabilità
Questa risposta si applica solo alle chiamate di funzione (ambiti nidificati non rimovibili sullo stesso thread).
(Nota a margine .) Quando le cose passabili possono sfuggire all'ambito (ovvero avere una durata che potenzialmente supera l'ambito esterno), diventa più importante soddisfare la necessità dell'applicazione di una gestione della durata dell'oggetto prima di ogni altra cosa. Di solito, questo richiede l'uso di riferimenti che sono anche in grado di gestire la vita, come i puntatori intelligenti. Un'alternativa potrebbe essere l'utilizzo di un manager. Si noti che, lambda è una sorta di ambito staccabile; Le acquisizioni lambda si comportano come se avessero un oggetto. Pertanto, fai attenzione con le acquisizioni lambda. Fai anche attenzione a come passa la lambda stessa, per copia o per riferimento.
Quando passare per valore
Per valori scalari (primitive standard che rientrano in un registro macchina e hanno valore semantico) per i quali non è necessario comunicare per mutabilità (riferimento condiviso), passare per valore.
Per le situazioni in cui la chiamata richiede una clonazione di un oggetto o di un aggregato, passare per valore, in cui la copia della chiamata soddisfa la necessità di un oggetto clonato.
Quando passare per riferimento, ecc.
per tutte le altre situazioni, passare da puntatori, riferimenti, puntatori intelligenti, maniglie (vedi: linguaggio del manico), ecc. Ogni volta che si segue questo consiglio, applicare il principio di correttezza const come al solito.
Le cose (aggregati, oggetti, matrici, strutture di dati) che hanno un ingombro della memoria sufficientemente ampio dovrebbero sempre essere progettate per facilitare il riferimento pass-by, per motivi di prestazioni. Questo consiglio si applica sicuramente quando si tratta di centinaia di byte o più. Questo consiglio è limite quando è di decine di byte.
Paradigmi insoliti
Esistono paradigmi di programmazione per scopi speciali che sono ricchi di intenzioni. Ad esempio, elaborazione di stringhe, serializzazione, comunicazione di rete, isolamento, wrapping di librerie di terze parti, comunicazione tra processi di memoria condivisa, ecc. In queste aree di applicazione o paradigmi di programmazione, i dati vengono copiati da strutture a strutture, o talvolta riconfezionati in array di byte.
In che modo le specifiche della lingua influiscono su questa risposta, prima di considerare l'ottimizzazione.
Sub-TL; DR La propagazione di un riferimento non deve invocare alcun codice; il passaggio per riferimento const soddisfa questo criterio. Tuttavia, tutte le altre lingue soddisfano questo criterio senza sforzo.
(Si consiglia ai programmatori principianti C ++ di saltare completamente questa sezione.)
(L'inizio di questa sezione è in parte ispirato dalla risposta di gnasher729. Tuttavia, si raggiunge una conclusione diversa.)
C ++ consente costruttori di copie e operatori di assegnazione definiti dall'utente.
(Questa è (è stata) una scelta audace che è (è stata) sia sorprendente che deplorevole. È sicuramente una divergenza dalla norma accettabile di oggi nella progettazione del linguaggio.)
Anche se il programmatore C ++ non ne definisce uno, il compilatore C ++ deve generare tali metodi in base ai principi del linguaggio e quindi determinare se è necessario eseguire codice aggiuntivo diverso da memcpy
. Ad esempio, un class
/ struct
che contiene un std::vector
membro deve avere un costruttore di copia e un operatore di assegnazione non banali.
In altre lingue, i costruttori di copie e la clonazione di oggetti sono scoraggiati (tranne dove assolutamente necessario e / o significativo per la semantica dell'applicazione), poiché gli oggetti hanno una semantica di riferimento, in base alla progettazione del linguaggio. Queste lingue avranno in genere un meccanismo di garbage collection basato sulla raggiungibilità anziché sulla proprietà basata sull'ambito o sul conteggio dei riferimenti.
Quando un riferimento o un puntatore (incluso il riferimento const) viene passato in C ++ (o C), al programmatore viene assicurato che non verrà eseguito alcun codice speciale (funzioni definite dall'utente o generate dal compilatore), tranne la propagazione del valore dell'indirizzo (riferimento o puntatore). Questa è una chiarezza di comportamento che i programmatori C ++ trovano a loro agio.
Tuttavia, lo sfondo è che il linguaggio C ++ è inutilmente complicato, tale che questa chiarezza di comportamento è come un'oasi (un habitat sopravvissibile) da qualche parte intorno a una zona di ricaduta nucleare.
Per aggiungere più benedizioni (o insulti), C ++ introduce riferimenti universali (valori r) al fine di facilitare gli operatori di movimento definiti dall'utente (costruttori di movimenti e operatori di assegnazione di movimenti) con buone prestazioni. Ciò avvantaggia un caso d'uso estremamente rilevante (lo spostamento (trasferimento) di oggetti da un'istanza all'altra), riducendo la necessità di copia e clonazione profonda. Tuttavia, in altre lingue, è illogico parlare di tale spostamento di oggetti.
(Sezione fuori tema) Una sezione dedicata a un articolo "Vuoi velocità? Passa per valore!" scritto nel 2009 circa.
L'articolo è stato scritto nel 2009 e spiega la giustificazione del progetto per il valore r in C ++. Questo articolo presenta un valido argomento contrario alla mia conclusione nella sezione precedente. Tuttavia, l'esempio di codice dell'articolo e la dichiarazione di prestazione è stata a lungo confutata.
Sub-TL; DR Il design della semantica di valore r in C ++ consente Sort
, ad esempio, una semantica sorprendentemente elegante sul lato utente di una funzione. Questo elegante è impossibile da modellare (imitare) in altre lingue.
Una funzione di ordinamento viene applicata a un'intera struttura di dati. Come accennato in precedenza, sarebbe lento se fosse coinvolta molta copia. Come ottimizzazione delle prestazioni (che è praticamente rilevante), una funzione di ordinamento è progettata per essere distruttiva in parecchi linguaggi diversi dal C ++. Distruttivo significa che la struttura dei dati di destinazione viene modificata per raggiungere l'obiettivo di ordinamento.
In C ++, l'utente può scegliere di chiamare una delle due implementazioni: una distruttiva con prestazioni migliori o una normale che non modifica l'input. (Il modello è omesso per brevità.)
/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
std::vector<T> result(std::move(input)); /* destructive move */
std::sort(result.begin(), result.end()); /* in-place sorting */
return result; /* return-value optimization (RVO) */
}
/*caller specifically passes in read-only argument*/
std::vector<T> my_sort(const std::vector<T>& input)
{
/* reuse destructive implementation by letting it work on a clone. */
/* Several things involved; e.g. expiring temporaries as r-value */
/* return-value optimization, etc. */
return my_sort(std::vector<T>(input));
}
/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/
Oltre all'ordinamento, questa eleganza è utile anche nell'implementazione dell'algoritmo di ricerca mediana distruttiva in un array (inizialmente non ordinato), mediante partizionamento ricorsivo.
Tuttavia, si noti che la maggior parte delle lingue applica un ordinamento bilanciato dell'albero di ricerca binario all'ordinamento, invece di applicare un algoritmo di ordinamento distruttivo agli array. Pertanto, la rilevanza pratica di questa tecnica non è così elevata come sembra.
In che modo l'ottimizzazione del compilatore influisce su questa risposta
Quando l'inline (e anche l'ottimizzazione dell'intero programma / ottimizzazione del tempo di collegamento) viene applicata su più livelli di chiamate di funzione, il compilatore è in grado di vedere (a volte in modo esaustivo) il flusso di dati. Quando ciò accade, il compilatore può applicare molte ottimizzazioni, alcune delle quali possono eliminare la creazione di interi oggetti in memoria. In genere, quando si applica questa situazione, non importa se i parametri vengono passati per valore o per riferimento const, poiché il compilatore può analizzare in modo esauriente.
Tuttavia, se la funzione di livello inferiore chiama qualcosa che va oltre l'analisi (ad es. Qualcosa in una libreria diversa dalla compilation o un grafico di chiamata che è semplicemente troppo complicato), allora il compilatore deve ottimizzare in modo difensivo.
Oggetti più grandi di un valore di registro macchina potrebbero essere copiati da istruzioni esplicite di caricamento / archiviazione della memoria o da una chiamata alla memcpy
funzione venerabile . Su alcune piattaforme, il compilatore genera istruzioni SIMD per spostarsi tra due posizioni di memoria, ciascuna istruzione sposta decine di byte (16 o 32).
Discussione sulla questione della verbosità o del disordine visivo
I programmatori C ++ sono abituati a questo, cioè finché un programmatore non odia il C ++, il sovraccarico di scrivere o leggere il riferimento const nel codice sorgente non è orribile.
Le analisi costi-benefici potrebbero essere state fatte molte volte in precedenza. Non so se ce ne siano di quelli scientifici che dovrebbero essere citati. Immagino che la maggior parte delle analisi non siano scientifiche o non riproducibili.
Ecco cosa immagino (senza prove o riferimenti credibili) ...
- Sì, influisce sulle prestazioni del software scritto in questa lingua.
- Se i compilatori sono in grado di comprendere lo scopo del codice, potrebbe essere potenzialmente abbastanza intelligente da automatizzarlo
- Sfortunatamente, nei linguaggi che favoriscono la mutabilità (rispetto alla purezza funzionale), il compilatore classificherebbe la maggior parte delle cose come mutate, quindi la deduzione automatizzata della costanza rifiuterebbe la maggior parte delle cose come non costanti
- Il sovraccarico mentale dipende dalle persone; le persone che ritengono che ciò sia un alto sovraccarico mentale avrebbero rifiutato il C ++ come linguaggio di programmazione praticabile.