¿El paso por valor es un valor predeterminado razonable en C ++ 11?

142

En C ++ tradicional, pasar por valor a funciones y métodos es lento para objetos grandes, y generalmente está mal visto. En cambio, los programadores de C ++ tienden a pasar referencias, lo que es más rápido, pero que introduce todo tipo de preguntas complicadas sobre la propiedad y especialmente sobre la administración de memoria (en el caso de que el objeto esté asignado en un montón)

Ahora, en C ++ 11, tenemos referencias de Rvalue y constructores de movimiento, lo que significa que es posible implementar un objeto grande (como un std::vector) que es barato pasar por valor dentro y fuera de una función.

Entonces, ¿esto significa que el valor predeterminado debería ser pasar por valor para instancias de tipos como std::vectory std::string? ¿Qué pasa con los objetos personalizados? ¿Cuál es la nueva mejor práctica?

Derek Thurn
fuente
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). No entiendo cómo es complicado o problemático para la propiedad. ¿Se me puede pasar algo por alto?
iammilind
1
@iammilind: Un ejemplo de experiencia personal. Un hilo tiene un objeto de cadena. Se pasa a una función que genera otro subproceso, pero desconocido para el llamador, la función tomó la cadena como const std::string&y no una copia. El primer hilo salió ...
Zan Lynx
12
@ZanLynx: Eso suena como una función que claramente nunca fue diseñada para ser llamada como una función de hilo.
Nicol Bolas
55
De acuerdo con iammilind, no veo ningún problema. Pasar por referencia constante debe ser el valor predeterminado para los objetos "grandes" y por valor para los objetos más pequeños. Pondría el límite entre grande y pequeño en alrededor de 16 bytes (o 4 punteros en un sistema de 32 bits).
JN
3
¡Herb Sutter ha vuelto a lo básico! La presentación de Essentials of Modern C ++ en CppCon entró en bastante detalle sobre esto. Video aquí .
Chris Drew

Respuestas:

138

Es un valor predeterminado razonable si necesita hacer una copia dentro del cuerpo. Esto es lo que Dave Abrahams defiende :

Pauta: no copie los argumentos de su función. En su lugar, páselos por valor y deje que el compilador haga la copia.

En código esto significa que no hagas esto:

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

pero haz esto:

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

que tiene la ventaja de que la persona que llama puede usar fooasí:

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

y solo se realiza un trabajo mínimo. Necesitaría dos sobrecargas para hacer lo mismo con las referencias, void foo(T const&);y void foo(T&&);.

Con eso en mente, ahora escribí mis valiosos constructores como tales:

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

De lo contrario, pasar por referencia a consttodavía es razonable.

Luc Danton
fuente
29
+1, especialmente para el último bit :) No se debe olvidar que Move Constructors solo se puede invocar si no se espera que el objeto a partir del cual no se cambie después: SomeProperty p; for (auto x: vec) { x.foo(p); }por ejemplo, no cabe. Además, los constructores de movimiento tienen un costo (cuanto mayor es el objeto, mayor es el costo) mientras que const&son esencialmente gratuitos.
Matthieu M.
25
@MatthieuM. Pero es importante saber qué significa "cuanto mayor sea el objeto, mayor será el costo" del movimiento: "mayor" en realidad significa "cuantas más variables miembro tenga". Por ejemplo, mover un std::vectorcon un millón de elementos cuesta lo mismo que mover uno con cinco elementos, ya que solo se mueve el puntero a la matriz en el montón, no todos los objetos en el vector. Entonces, en realidad no es un problema tan grande.
Lucas
+1 También tiendo a usar la construcción de pasar por valor y luego mover desde que comencé a usar C ++ 11. Sin embargo, esto me hace sentir un poco incómodo, ya que mi código ahora tiene std::movetodo el lugar ..
stijn
1
Hay un riesgo con const&eso, que me ha hecho tropezar varias veces. void foo(const T&); int main() { S s; foo(s); }. Esto puede compilarse, aunque los tipos son diferentes, si hay un constructor T que toma una S como argumento. Esto puede ser lento, porque se puede construir un objeto T grande. Puede pensar que está pasando una referencia sin copiar, pero puede ser. Vea esta respuesta a una pregunta que hice más. Básicamente, &generalmente se une solo a los valores, pero hay una excepción para rvalue. Hay alternativas
Aaron McDaid el
1
@AaronMcDaid Esta es una noticia vieja, en el sentido de que es algo de lo que siempre debes estar al tanto, incluso antes de C ++ 11. Y nada ha cambiado con respecto a eso.
Luc Danton
71

En casi todos los casos, su semántica debe ser:

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

Todas las demás firmas deben usarse con moderación y con buena justificación. El compilador ahora casi siempre los resolverá de la manera más eficiente. ¡Puedes seguir escribiendo tu código!

Ayjay
fuente
2
Aunque prefiero pasar un puntero si voy a modificar un argumento. Estoy de acuerdo con la guía de estilo de Google en que esto hace que sea más obvio que el argumento se modificará sin necesidad de verificar la firma de la función ( google-styleguide.googlecode.com/svn/trunk/… ).
Max Lybbert
40
La razón por la que no me gusta pasar punteros es que agrega un posible estado de falla a mis funciones. Trato de escribir todas mis funciones para que sean probadamente correctas, ya que reduce enormemente el espacio para que los errores se escondan. foo(bar& x) { x.a = 3; }Es muchísimo más confiable (¡y legible!) Quefoo(bar* x) {if (!x) throw std::invalid_argument("x"); x->a = 3;
Ayjay
22
@Max Lybbert: con un parámetro de puntero, no necesita verificar la firma de la función, pero debe verificar la documentación para saber si puede pasar punteros nulos, si la función tomará posesión, etc. En mi humilde opinión, Un parámetro puntero transmite mucha menos información que una referencia no constante. Sin embargo, estoy de acuerdo en que sería bueno tener una pista visual en el sitio de la llamada de que el argumento puede modificarse (como la refpalabra clave en C #).
Luc Touraille
Con respecto a pasar por valor y confiar en la semántica de movimiento, creo que estas tres opciones explican mejor el uso previsto del parámetro. Estas son las pautas que siempre sigo también.
Trevor Hickey
1
@AaronMcDaid is shared_ptr intended to never be null? Much as (I think) unique_ptr is?Ambos supuestos son incorrectos. unique_ptry shared_ptrpuede contener nulo / nullptrvalores. Si no desea preocuparse por los valores nulos, debe usar referencias, porque nunca pueden ser nulas. Tampoco tendrá que escribir ->, lo que le resultará molesto :)
Julian
10

Pase los parámetros por valor si dentro del cuerpo de la función necesita una copia del objeto o solo necesita mover el objeto. Pase const&si solo necesita acceso no mutante al objeto.

Ejemplo de copia de objeto:

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();
}

Ejemplo de movimiento de objeto:

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)); 
}

Ejemplo de acceso no mutante:

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

Para una explicación, vea estas publicaciones de blog de Dave Abrahams y Xiang Fan .

Edward Brey
fuente
0

La firma de una función debe reflejar su uso previsto. La legibilidad es importante, también para el optimizador.

Esta es la mejor condición previa para que un optimizador cree un código más rápido, al menos en teoría y, si no en la realidad, en unos pocos años.

Las consideraciones de rendimiento a menudo se sobrevaloran en el contexto del paso de parámetros. El reenvío perfecto es un ejemplo. Las funciones como emplace_backson en su mayoría muy cortas e integradas de todos modos.

Patrick Fromberg
fuente