Il pass-by-value è un valore predefinito ragionevole in C ++ 11?


142

Nel C ++ tradizionale, passare per valore in funzioni e metodi è lento per oggetti di grandi dimensioni e generalmente non viene visto. Invece, i programmatori C ++ tendono a passare i riferimenti in giro, che è più veloce, ma che introduce ogni sorta di domande complicate sulla proprietà e in particolare sulla gestione della memoria (nel caso in cui l'oggetto sia allocato in heap)

Ora, in C ++ 11, abbiamo riferimenti a Rvalue e spostiamo i costruttori, il che significa che è possibile implementare un oggetto di grandi dimensioni (come un std::vector) che è economico per passare da un valore all'altro in una funzione.

Quindi, ciò significa che il valore predefinito dovrebbe essere passare per valore per istanze di tipi come std::vectore std::string? Che dire di oggetti personalizzati? Qual è la nuova migliore pratica?


22
pass by reference ... which introduces all sorts of complicated questions around ownership and especially around memory management (in the event that the object is heap-allocated). Non capisco come sia complicato o problematico per la proprietà? Potrei essermi perso qualcosa?
iammilind,

1
@iammilind: un esempio per esperienza personale. Un thread ha un oggetto stringa. Viene passato a una funzione che genera un altro thread, ma sconosciuta al chiamante la funzione ha preso la stringa come const std::string&e non una copia. Il primo thread è quindi uscito ...
Zan Lynx,

12
@ZanLynx: Sembra una funzione che chiaramente non è mai stata progettata per essere chiamata come funzione thread.
Nicol Bolas,

5
D'accordo con iammilind, non vedo alcun problema. Il passaggio per riferimento const dovrebbe essere l'impostazione predefinita per oggetti "grandi" e in base al valore per oggetti più piccoli. Metterei il limite tra grande e piccolo a circa 16 byte (o 4 puntatori su un sistema a 32 bit).
JN,

3
Herb Sutter è tornato alle origini! Gli elementi essenziali della presentazione moderna di C ++ al CppCon sono stati approfonditi su questo. Video qui .
Chris Drew,

Risposte:


138

È un valore predefinito ragionevole se è necessario effettuare una copia all'interno del corpo. Questo è ciò che Dave Abrahams sta sostenendo :

Linea guida: non copiare gli argomenti della funzione. Invece, passali per valore e lascia che il compilatore esegua la copia.

Nel codice questo significa non farlo:

void foo(T const& t)
{
    auto copy = t;
    // ...
}

ma fai questo:

void foo(T t)
{
    // ...
}

che ha il vantaggio che il chiamante può usare in questo foomodo:

T lval;
foo(lval); // copy from lvalue
foo(T {}); // (potential) move from prvalue
foo(std::move(lval)); // (potential) move from xvalue

e viene svolto solo un lavoro minimo. Avresti bisogno di due sovraccarichi per fare lo stesso con i riferimenti void foo(T const&);e void foo(T&&);.

Con questo in mente, ora ho scritto i miei stimati costruttori come tali:

class T {
    U u;
    V v;
public:
    T(U u, V v)
        : u(std::move(u))
        , v(std::move(v))
    {}
};

Altrimenti, passare per riferimento a constStill è ragionevole.


29
+1, in particolare per l'ultimo bit :) Non si deve dimenticare che Move Constructors può essere invocato solo se non si prevede che l'oggetto da spostare in seguito rimanga invariato: SomeProperty p; for (auto x: vec) { x.foo(p); }non si adatta, ad esempio. Inoltre, Move Constructors ha un costo (maggiore è l'oggetto, maggiore è il costo) mentre const&è essenzialmente gratuito.
Matthieu M.

25
@MatthieuM. Ma è importante sapere cosa significa "più grande è l'oggetto, maggiore è il costo" della mossa: "più grande" significa in realtà "più variabili membro ha". Ad esempio, spostare un std::vectorcon un milione di elementi costa lo stesso di uno con cinque elementi poiché viene spostato solo il puntatore alla matrice sull'heap, non tutti gli oggetti nel vettore. Quindi in realtà non è un grosso problema.
Lucas,

+1 Tendo anche a usare il costrutto pass-by-value-then-move da quando ho iniziato a usare C ++ 11. Questo mi fa sentire un po 'a disagio, dato che il mio codice ora ha std::movetutto il posto ..
stijn

1
C'è un rischio const&, che mi ha fatto inciampare alcune volte. void foo(const T&); int main() { S s; foo(s); }. Questo può essere compilato, anche se i tipi sono diversi, se esiste un costruttore T che accetta una S come argomento. Questo può essere lento, perché potrebbe essere costruito un oggetto T di grandi dimensioni. Potresti pensare di passare un riferimento senza copiare, ma potresti esserlo. Vedi questa risposta a una domanda che ho chiesto di più. Fondamentalmente, di &solito si lega solo ai valori, ma c'è un'eccezione per rvalue. Ci sono alternative
Aaron McDaid,

1
@AaronMcDaid Questa è una vecchia notizia, nel senso che è qualcosa di cui hai sempre dovuto essere a conoscenza anche prima di C ++ 11. E nulla è cambiato rispetto a questo.
Luc Danton,

71

In quasi tutti i casi, la semantica dovrebbe essere:

bar(foo f); // want to obtain a copy of f
bar(const foo& f); // want to read f
bar(foo& f); // want to modify f

Tutte le altre firme dovrebbero essere usate solo con parsimonia e con una buona giustificazione. Il compilatore ora quasi sempre li risolverà nel modo più efficiente. Puoi continuare a scrivere il tuo codice!


2
Anche se preferisco passare un puntatore se ho intenzione di modificare un argomento. Concordo con la guida di stile di Google che ciò rende più ovvio che l'argomento verrà modificato senza bisogno di ricontrollare la firma della funzione ( google-styleguide.googlecode.com/svn/trunk/… ).
Max Lybbert,

40
Il motivo per cui non mi piace passare i puntatori è che aggiunge un possibile stato di errore alle mie funzioni. Cerco di scrivere tutte le mie funzioni in modo che siano dimostrabilmente corrette, in quanto riduce notevolmente lo spazio in cui nascondersi i bug. foo(bar& x) { x.a = 3; }È un diavolo di molto più affidabile (e leggibile!) Difoo(bar* x) {if (!x) throw std::invalid_argument("x"); x->a = 3;
Ayjay

22
@Max Lybbert: con un parametro pointer, non è necessario controllare la firma della funzione, ma è necessario controllare la documentazione per sapere se è consentito passare puntatori null, se la funzione prenderà la proprietà, ecc. IMHO, un parametro pointer trasmette molte meno informazioni di un riferimento non const. Sono d'accordo tuttavia che sarebbe bello avere un indizio visivo sul sito della chiamata che l'argomento potrebbe essere modificato (come la refparola chiave in C #).
Luc Touraille,

Per quanto riguarda il passaggio in base al valore e il fatto di basarsi sulla semantica di movimento, ritengo che queste tre scelte facciano un lavoro migliore nel spiegare l'uso previsto del parametro. Queste sono anche le linee guida che seguo sempre.
Trevor Hickey,

1
@AaronMcDaid is shared_ptr intended to never be null? Much as (I think) unique_ptr is?Entrambe queste ipotesi sono errate. unique_ptre shared_ptrpuò contenere nullptrvalori / null . Se non vuoi preoccuparti dei valori null, dovresti usare i riferimenti, perché non possono mai essere null. Inoltre non dovrai digitare ->, che ritieni fastidioso :)
Julian

10

Passa i parametri per valore se all'interno del corpo della funzione hai bisogno di una copia dell'oggetto o devi solo spostare l'oggetto. Passa const&se hai bisogno solo di un accesso non mutante all'oggetto.

Esempio di copia dell'oggetto:

void copy_antipattern(T const& t) { // (Don't do this.)
    auto copy = t;
    t.some_mutating_function();
}

void copy_pattern(T t) { // (Do this instead.)
    t.some_mutating_function();
}

Esempio di spostamento dell'oggetto:

std::vector<T> v; 

void move_antipattern(T const& t) {
    v.push_back(t); 
}

void move_pattern(T t) {
    v.push_back(std::move(t)); 
}

Esempio di accesso non mutante:

void read_pattern(T const& t) {
    t.some_const_function();
}

Per ragioni logiche, vedi questi post sul blog di Dave Abrahams e Xiang Fan .


0

La firma di una funzione dovrebbe riflettere l'uso previsto. La leggibilità è importante, anche per l'ottimizzatore.

Questo è il miglior presupposto per un ottimizzatore per creare il codice più veloce - almeno in teoria e se non nella realtà, poi tra qualche anno la realtà.

Le considerazioni sulle prestazioni sono spesso sopravvalutate nel contesto del passaggio dei parametri. L'inoltro perfetto è un esempio. Le funzioni come emplace_backsono per lo più molto brevi e in linea comunque.

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.