c++ - retornan - Pase por valor vs pase por referencia de valor
paso de datos por referencia en c (6)
El parámetro de referencia rvalue te obliga a ser explícito acerca de las copias.
Sí, pass-by-rvalue-reference tiene un punto.
El parámetro de referencia rvalue significa que puede mover el argumento, pero no lo manda.
Sí, el paso por valor tiene un punto. Pero eso también le da a pass-by-rvalue la oportunidad de manejar la garantía de excepción: si foo
throws, el valor del widget
no es necesario consumido.
Para los tipos de solo movimiento (como std::unique_ptr
), el paso por valor parece ser la norma (principalmente para el segundo punto, y el primer punto no es aplicable de todos modos).
EDITAR: la biblioteca estándar contradice mi oración anterior, uno de los constructores de shared_ptr
toma std::unique_ptr<T, D>&&
.
Para los tipos que tienen copia / movimiento (como std::shared_ptr
), tenemos la opción de la coherencia con los tipos anteriores o la fuerza para ser explícitos en la copia.
A menos que quiera garantizar que no hay una copia no deseada, usaría el paso por valor para la coherencia. A menos que desee un sumidero garantizado y / o inmediato, usaría el valor de paso por valor.
Para el código base existente, mantendría la consistencia.
¿Cuándo debo declarar mi función como:
void foo(Widget w);
Opuesto a
void foo(Widget&& w);
?
Supongamos que esta es la única sobrecarga (como en, escojo una u otra, no ambas, y ninguna otra sobrecarga). No hay plantillas involucradas. Supongamos que la función foo
requiere la propiedad del Widget
(por ejemplo, el const Widget&
no es parte de esta discusión). No estoy interesado en ninguna respuesta fuera del alcance de estas circunstancias. Consulte el apéndice al final del post para ver por qué estas restricciones son parte de la pregunta.
La principal diferencia que mis colegas y yo podemos encontrar es que el parámetro rvalue reference te obliga a ser explícito sobre las copias. La persona que llama es responsable de hacer una copia explícita y luego pasarla con std::move
cuando desee una copia. En el caso de pasar por valor, el costo de la copia está oculto:
//If foo is a pass by value function, calling + making a copy:
Widget x{};
foo(x); //Implicit copy
//Not shown: continues to use x locally
//If foo is a pass by rvalue reference function, calling + making a copy:
Widget x{};
//foo(x); //This would be a compiler error
auto copy = x; //Explicit copy
foo(std::move(copy));
//Not shown: continues to use x locally
Aparte de esa diferencia. Además de obligar a las personas a ser explícitas sobre cómo copiar y cambiar la cantidad de azúcar sintáctica que obtiene cuando llama a la función, ¿de qué otra manera son diferentes? ¿Qué dicen de manera diferente sobre la interfaz? ¿Son más o menos eficientes que el otro?
Otras cosas que mis colegas y yo ya hemos pensado:
- El parámetro de referencia rvalue significa que puede mover el argumento, pero no lo manda. Es posible que el argumento que haya pasado en el sitio de la llamada esté en su estado original después. También es posible que la función comiera / cambiara el argumento sin llamar nunca a un constructor de movimientos, pero suponga que, debido a que era una referencia de valor, la persona que llama abandonó el control. Pase por valor, si se mueve en él, debe asumir que se produjo un movimiento; no hay elección
- Suponiendo que no haya uniones, una sola llamada de constructor de movimiento se elimina con paso por valor.
- El compilador tiene una mejor oportunidad de evitar copias / movimientos con el paso por valor. ¿Alguien puede justificar esta afirmación? Preferiblemente, con un enlace a gcc.godbolt.org que muestra el código generado optimizado de gcc / clang en lugar de una línea en el estándar. Mi intento de mostrar esto probablemente no pudo aislar correctamente el comportamiento: https://godbolt.org/g/4yomtt
Anexo: ¿Por qué estoy limitando tanto este problema?
- Sin sobrecargas: si hubiera otras sobrecargas, esto se convertiría en una discusión de paso por valor frente a un conjunto de sobrecargas que incluyen tanto la referencia constante como la referencia de valor, en cuyo punto el conjunto de sobrecargas es obviamente más eficiente y gana. Esto es bien conocido, y por lo tanto no es interesante.
- Sin plantillas: no me interesa cómo encajan las referencias de reenvío en la imagen. Si tiene una referencia de reenvío, llame a std :: forward de todos modos. El objetivo con una referencia de reenvío es pasar las cosas como las recibiste. Las copias no son relevantes porque simplemente pasas un valor l. Es bien conocido, y no es interesante.
-
foo
requiere la propiedad deWidget
(también conocido como noconst Widget&
) - No estamos hablando de funciones de solo lectura. Si la función era de solo lectura o no necesitaba poseer o extender la vida útil delWidget
, entonces la respuesta se convierte en unaconst Widget&
, lo que, de nuevo, es bien conocido y no es interesante. También te refiero a por qué no queremos hablar de sobrecargas.
¿Qué dicen los usos de rvalue sobre una interfaz en lugar de copiar? rvalue sugiere a la persona que llama que la función quiere poseer el valor y no tiene la intención de que la persona que llama sepa de los cambios que ha realizado. Considere lo siguiente (sé que usted dijo que no hay referencias de valores en su ejemplo, pero tenga paciencia conmigo):
void fn(RAII &&);
RAII x{underlying_resource};
fn(std::move(x));
// later in the code
RAII y{underlying_resource};
Para otra forma de verlo:
vector<B> fn1(vector<A> &&x);
vector<C> fn2(vector<B> &&x);
vector<A> va; // large vector
vector<B> vb = fn1(std::move(va));
vector<C> vc = fn2(std::move(vb));
Ahora, si los Widgets tienen que seguir siendo únicos (como los widgets reales en, digamos, hacer GTK), entonces la primera opción no puede funcionar. La segunda, tercera y cuarta opciones tienen sentido, porque todavía hay una sola representación real de los datos. De todos modos, eso es lo que me dicen esas semánticas cuando las veo en código.
Ahora, en cuanto a la eficiencia: depende. Las referencias de valor pueden ahorrar mucho tiempo si Widget tiene un puntero a un miembro de datos cuyo contenido apuntado puede ser bastante grande (piense en una matriz). Como la persona que llama usó un valor, están diciendo que ya no les importa lo que le están dando. Por lo tanto, si desea mover el contenido del Widget de la persona que llama a su Widget, simplemente tome su puntero. No es necesario copiar meticulosamente cada elemento de la estructura de datos a la que apunta el puntero. Esto puede conducir a mejoras bastante buenas en la velocidad (nuevamente, piense en matrices). Pero si la clase Widget no tiene tal cosa, este beneficio no se ve por ninguna parte.
Con suerte eso llega a lo que estabas preguntando; Si no, tal vez pueda ampliar / aclarar las cosas.
A menos que el tipo sea solo de movimiento, normalmente tiene la opción de pasar por referencia a const y parece arbitrario que sea "no parte de la discusión", pero lo intentaré.
Creo que la elección depende en parte de lo que foo
va a hacer con el parámetro.
La función necesita una copia local.
Digamos que Widget
es un iterador y desea implementar su propia función std::next
. Luego necesita su propia copia para avanzar y luego regresar. En este caso su elección es algo como:
Widget next(Widget it, int n = 1){
std::advance(it, n);
return it;
}
vs
Widget next(Widget&& it, int n = 1){
std::advance(it, n);
return std::move(it);
}
Creo que el valor es mejor aquí. Desde la firma se puede ver que está sacando una copia. Si la persona que llama quiere evitar una copia, puede hacer un std::move
y garantizar que la variable se mueva, pero aún así pueden pasar valores de l, si lo desean. Con pass-by-rvalue-reference, la persona que llama no puede garantizar que la variable se haya movido.
Mover la asignación a una copia.
Digamos que tienes una clase WidgetHolder
:
class WidgetHolder {
Widget widget;
//...
};
y necesitas implementar una función miembro de setWidget
. Voy a asumir que ya tienes una sobrecarga que requiere una referencia a const:
WidgetHolder::setWidget(const Widget& w) {
widget = w;
}
pero después de medir el rendimiento, decide que necesita optimizar los valores r. Usted tiene la opción de reemplazarlo con:
WidgetHolder::setWidget(Widget w) {
widget = std::move(w);
}
O sobrecargando con:
WidgetHolder::setWidget(Widget&& widget) {
widget = std::move(w);
}
Este es un poco más complicado. Es tentador elegir el paso por valor porque acepta tanto rvalues como lvalues para que no necesite dos sobrecargas. Sin embargo, es incondicional tomar una copia, por lo que no puede aprovechar ninguna capacidad existente en la variable miembro. El paso por referencia a const y el paso por sobrecargas de referencia de valor r utilizan la asignación sin tomar una copia que podría ser más rápida
Mover-construir una copia
Ahora digamos que está escribiendo el constructor para WidgetHolder
y como antes ya ha implementado un constructor que toma una referencia a const:
WidgetHolder::WidgetHolder(const Widget& w) : widget(w) {
}
y como antes, ha medido el rendimiento y ha decidido que necesita optimizar los valores. Usted tiene la opción de reemplazarlo con:
WidgetHolder::WidgetHolder(Widget w) : widget(std::move(w)) {
}
O sobrecargando con:
WidgetHolder::WidgetHolder(Widget&& w) : widget(std:move(w)) {
}
En este caso, la variable miembro no puede tener ninguna capacidad existente ya que este es el constructor. Estás moviendo-construyendo una copia. Además, los constructores a menudo toman muchos parámetros, por lo que puede ser bastante difícil escribir todas las diferentes permutaciones de sobrecargas para optimizar las referencias de valor r. Por lo tanto, en este caso, es una buena idea usar el paso por valor, especialmente si el constructor toma muchos de estos parámetros.
Pasando unique_ptr
Con unique_ptr
las preocupaciones de eficiencia son menos importantes dado que un movimiento es tan barato y no tiene ninguna capacidad. Más importante es la expresividad y la corrección. Hay una buena discusión sobre cómo pasar unique_ptr
here .
Cuando se pasa por un valor de referencia, la vida útil de los objetos se complica. Si la persona que llama no se aleja del argumento, la destrucción del argumento se retrasa. Creo que esto es interesante en dos casos.
Primero, tienes una clase RAII
//Hello. I want my own local copy of your Widget that I will manipulate,
//but I don''t want my changes to affect the one you have. I may or may not
//hold onto it for later, but that''s none of your business.
void foo(Widget w);
//Hello. I want to take your Widget and play with it. It may be in a
//different state than when you gave it to me, but it''ll still be yours
//when I''m finished. Trust me!
void foo(Widget& w);
//Hello. Can I see that Widget of yours? I don''t want to mess with it;
//I just want to check something out on it. Read that one value from it,
//or observe what state it''s in. I won''t touch it and I won''t keep it.
void foo(const Widget& w);
//Hello. Ooh, I like that Widget you have. You''re not going to use it
//anymore, are you? Please just give it to me. Thank you! It''s my
//responsibility now, so don''t worry about it anymore, m''kay?
void foo(Widget&& w);
Al inicializar y
, el recurso aún podría mantenerse por x
si fn
no se mueve fuera de la referencia rvalue. En el código de paso por valor, sabemos que x
sale de fn
, y fn
lanza x
. Este es probablemente un caso en el que querría pasar por valor, y el constructor de copias probablemente se eliminaría, por lo que no tendría que preocuparse por copias accidentales.
En segundo lugar, si el argumento es un objeto grande y la función no se mueve, la vida útil de los datos vectoriales es mayor que en el caso de pasar por valor.
//Here, let me buy you a new car just like mine. I don''t care if you wreck
//it or give it a new paint job; you have yours and I have mine.
void foo(Car c);
//Here are the keys to my car. I understand that it may come back...
//not quite the same... as I lent it to you, but I''m okay with that.
void foo(Car& c);
//Here are the keys to my car as long as you promise to not give it a
//paint job or anything like that
void foo(const Car& c);
//I don''t need my car anymore, so I''m signing the title over to you now.
//Happy birthday!
void foo(Car&& c);
En el ejemplo anterior, si fn1
y fn1
no se mueven fuera de x
, terminarás con todos los datos de todos los vectores que aún están vivos. Si, en cambio, pasa por valor, solo los datos del último vector seguirán vivos (suponiendo que los vectores mueven el constructor borra el vector de fuentes).
La elección entre by-value y by-rvalue-ref, sin otras sobrecargas, no es significativa.
Con paso por valor, el argumento real puede ser una expresión de valor l.
Con pass by rvalue-ref, el argumento real debe ser un rvalue.
Si la función está almacenando una copia del argumento, entonces una elección sensata es entre paso por valor y un conjunto de sobrecargas con paso por ref a const y pass-by-rvalue-ref. Para una expresión rvalue como argumento real, el conjunto de sobrecargas puede evitar un movimiento. Es una decisión de ingenio de la ingeniería si la microoptimización vale la complejidad agregada y la tipificación.
Un problema que no se menciona en las otras respuestas es la idea de excepción-seguridad.
En general, si la función lanza una excepción, lo ideal sería que tuviéramos una garantía de excepción sólida , lo que significa que la llamada no tiene más efecto que la excepción. Si el paso por valor usa el constructor de movimiento, tal efecto es esencialmente inevitable. Por lo tanto, un argumento de referencia de rvalor puede ser superior en algunos casos. (Por supuesto, hay varios casos en los que la fuerte garantía de excepción no se puede lograr de ninguna manera, así como varios casos donde la garantía de no tirar está disponible de cualquier manera. Por lo tanto, esto no es relevante en el 100% de los casos. Pero es relevante algunas veces.)