LA PREGUNTA MÁS IMPORTANTE PRIMERO:
¿Existen mejores prácticas sobre cómo enviar parámetros en C ++ porque realmente lo encuentro, digamos, no trivial?
Si su función necesita modificar el objeto original que se está pasando, de modo que después de que la llamada regrese, las modificaciones a ese objeto serán visibles para la persona que llama, entonces debe pasar por la referencia lvalue :
void foo(my_class& obj)
{
// Modify obj here...
}
Si su función no necesita modificar el objeto original y no necesita crear una copia del mismo (en otras palabras, solo necesita observar su estado), entonces debe pasar por referencia lvalue aconst
:
void foo(my_class const& obj)
{
// Observe obj here
}
Esto le permitirá llamar a la función tanto con lvalues (lvalues son objetos con una identidad estable) como con rvalues (rvalues son, por ejemplo , temporales u objetos de los que está a punto de moverse como resultado de la llamada std::move()
).
También se podría argumentar que para los tipos fundamentales o tipos para los que la copia es rápida , como int
, bool
o char
, no hay necesidad de pasar por referencia si la función simplemente necesita observar el valor, y debería favorecerse el paso por valor . Eso es correcto si la semántica de referencia no es necesaria, pero ¿qué pasa si la función quisiera almacenar un puntero a ese mismo objeto de entrada en algún lugar, de modo que las lecturas futuras a través de ese puntero vean las modificaciones de valor que se han realizado en alguna otra parte del ¿código? En este caso, pasar por referencia es la solución correcta.
Si su función no necesita modificar el objeto original, pero necesita almacenar una copia de ese objeto ( posiblemente para devolver el resultado de una transformación de la entrada sin alterar la entrada ), entonces podría considerar tomar por valor :
void foo(my_class obj) // One copy or one move here, but not working on
// the original object...
{
// Working on obj...
// Possibly move from obj if the result has to be stored somewhere...
}
La invocación de la función anterior siempre resultará en una copia al pasar lvalues, y en una se mueve al pasar rvalues. Si su función necesita almacenar este objeto en algún lugar, puede realizar un movimiento adicional desde él (por ejemplo, en el caso de que foo()
sea una función miembro que necesita almacenar el valor en un miembro de datos ).
En caso de que los movimientos sean costosos para los objetos de tipo my_class
, entonces puede considerar la sobrecarga foo()
y proporcionar una versión para lvalues (aceptando una referencia de lvalue a const
) y una versión para rvalues (aceptando una referencia de rvalue):
// Overload for lvalues
void foo(my_class const& obj) // No copy, no move (just reference binding)
{
my_class copyOfObj = obj; // Copy!
// Working on copyOfObj...
}
// Overload for rvalues
void foo(my_class&& obj) // No copy, no move (just reference binding)
{
my_class copyOfObj = std::move(obj); // Move!
// Notice, that invoking std::move() is
// necessary here, because obj is an
// *lvalue*, even though its type is
// "rvalue reference to my_class".
// Working on copyOfObj...
}
Las funciones anteriores son tan similares, de hecho, que podría hacer una función única: foo()
podría convertirse en una plantilla de función y podría usar el reenvío perfecto para determinar si un movimiento o una copia del objeto que se está pasando se generará internamente:
template<typename C>
void foo(C&& obj) // No copy, no move (just reference binding)
// ^^^
// Beware, this is not always an rvalue reference! This will "magically"
// resolve into my_class& if an lvalue is passed, and my_class&& if an
// rvalue is passed
{
my_class copyOfObj = std::forward<C>(obj); // Copy if lvalue, move if rvalue
// Working on copyOfObj...
}
Es posible que desee obtener más información sobre este diseño viendo esta charla de Scott Meyers (solo tenga en cuenta el hecho de que el término " Referencias universales " que está usando no es estándar).
Una cosa a tener en cuenta es que std::forward
generalmente terminará en un movimiento para rvalues, por lo que aunque parezca relativamente inocente, reenviar el mismo objeto varias veces puede ser una fuente de problemas, por ejemplo, ¡moverse desde el mismo objeto dos veces! Así que tenga cuidado de no poner esto en un bucle y no reenviar el mismo argumento varias veces en una llamada de función:
template<typename C>
void foo(C&& obj)
{
bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous!
}
También tenga en cuenta que normalmente no recurre a la solución basada en plantillas a menos que tenga una buena razón para ello, ya que hace que su código sea más difícil de leer. Normalmente, debería centrarse en la claridad y la sencillez .
Las anteriores son solo pautas simples, pero la mayoría de las veces le indicarán buenas decisiones de diseño.
ACERCA DEL RESTO DE SU PUESTO:
Si lo reescribo como [...] habrá 2 movimientos y ninguna copia.
Esto no es correcto. Para empezar, una referencia rvalue no se puede vincular a un lvalue, por lo que esto solo se compilará cuando pase un rvalue de tipo CreditCard
a su constructor. Por ejemplo:
// Here you are passing a temporary (OK! temporaries are rvalues)
Account acc("asdasd",345, CreditCard("12345",2,2015,1001));
CreditCard cc("12345",2,2015,1001);
// Here you are passing the result of std::move (OK! that's also an rvalue)
Account acc("asdasd",345, std::move(cc));
Pero no funcionará si intenta hacer esto:
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc); // ERROR! cc is an lvalue
Porque cc
es un lvalue y las referencias de rvalue no se pueden enlazar con lvalues. Además, cuando se vincula una referencia a un objeto, no se realiza ningún movimiento : es solo una vinculación de referencia. Por lo tanto, solo habrá un movimiento.
Entonces, según las pautas proporcionadas en la primera parte de esta respuesta, si le preocupa la cantidad de movimientos que se generan cuando toma un CreditCard
valor por valor, podría definir dos sobrecargas de constructor, una tomando una referencia de lvalue a const
( CreditCard const&
) y otra tomando una referencia rvalue ( CreditCard&&
).
La resolución de sobrecarga seleccionará el primero al pasar un lvalue (en este caso, se realizará una copia) y el segundo al pasar un rvalue (en este caso, se realizará un movimiento).
Account(std::string number, float amount, CreditCard const& creditCard)
: number(number), amount(amount), creditCard(creditCard) // copy here
{ }
Account(std::string number, float amount, CreditCard&& creditCard)
: number(number), amount(amount), creditCard(std::move(creditCard)) // move here
{ }
El uso de std::forward<>
normalmente se ve cuando desea lograr un reenvío perfecto . En ese caso, su constructor sería en realidad una plantilla de constructor y se vería más o menos de la siguiente manera
template<typename C>
Account(std::string number, float amount, C&& creditCard)
: number(number), amount(amount), creditCard(std::forward<C>(creditCard)) { }
En cierto sentido, esto combina las sobrecargas que he mostrado anteriormente en una sola función: C
se deducirá CreditCard&
en caso de que esté pasando un lvalue, y debido a las reglas de colapso de referencia, hará que esta función sea instanciada:
Account(std::string number, float amount, CreditCard& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard&>(creditCard))
{ }
Esto provocará una construcción de copia de creditCard
, como desearía. Por otro lado, cuando se pasa un rvalue, C
se deducirá que es CreditCard
, y esta función se instanciará en su lugar:
Account(std::string number, float amount, CreditCard&& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard))
{ }
Esto provocará una construcción de movimiento de creditCard
, que es lo que desea (porque el valor que se pasa es un valor r, y eso significa que estamos autorizados a movernos de él).