Soy un principiante en C ++ pero no un principiante en programación. Estoy tratando de aprender C ++ (c ++ 11) y no me queda claro lo más importante: pasar parámetros.
Consideré estos simples ejemplos:
Una clase que tiene todos sus miembros tipos primitivos:
CreditCard(std::string number, int expMonth, int expYear,int pin):number(number), expMonth(expMonth), expYear(expYear), pin(pin)
Una clase que tiene como miembros tipos primitivos + 1 tipo complejo:
Account(std::string number, float amount, CreditCard creditCard) : number(number), amount(amount), creditCard(creditCard)
Una clase que tiene como miembros tipos primitivos + 1 colección de algún tipo complejo:
Client(std::string firstName, std::string lastName, std::vector<Account> accounts):firstName(firstName), lastName(lastName), accounts(accounts)
Cuando creo una cuenta, hago esto:
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc);
Obviamente, la tarjeta de crédito se copiará dos veces en este escenario. Si reescribo ese constructor como
Account(std::string number, float amount, CreditCard& creditCard)
: number(number)
, amount(amount)
, creditCard(creditCard)
habrá una copia. Si lo reescribo como
Account(std::string number, float amount, CreditCard&& creditCard)
: number(number)
, amount(amount)
, creditCard(std::forward<CreditCard>(creditCard))
Habrá 2 movimientos y ninguna copia.
Creo que a veces es posible que desee copiar algún parámetro, a veces no desea copiar cuando crea ese objeto.
Vengo de C # y, al estar acostumbrado a las referencias, me resulta un poco extraño y creo que debería haber 2 sobrecargas para cada parámetro, pero sé que estoy equivocado.
¿Existen mejores prácticas sobre cómo enviar parámetros en C ++ porque realmente lo encuentro, digamos, no trivial? ¿Cómo manejaría mis ejemplos presentados anteriormente?
std::string
es una claseCreditCard
, no un tipo primitivo."abc"
no es de tipostd::string
, ni de tipochar */const char *
, sino de tipoconst char[N]
(con N = 4 en este caso debido a tres caracteres y un nulo). Esa es una idea errónea común y buena de apartarse del camino.Respuestas:
LA PREGUNTA MÁS IMPORTANTE PRIMERO:
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 :
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 a
const
: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
ochar
, 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 :
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 sobrecargafoo()
y proporcionar una versión para lvalues (aceptando una referencia de lvalue aconst
) y una versión para rvalues (aceptando una referencia de rvalue):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: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: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:
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:Pero no funcionará si intenta hacer esto:
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 aconst
(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).
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 maneraEn 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:Esto provocará una construcción de copia de
creditCard
, como desearía. Por otro lado, cuando se pasa un rvalue,C
se deducirá que esCreditCard
, y esta función se instanciará en su lugar: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).fuente
const
. Si necesita modificar el objeto original, tome por referencia a non-`const. Si necesita hacer una copia y los movimientos son baratos, tome por valor y luego muévase.obj
lugar de hacer una copia local para mudarte, ¿no?std::forward
se puede llamar una vez . He visto a personas ponerlo en bucles, etc. y dado que esta respuesta será vista por muchos principiantes, en mi humilde opinión debería haber una gruesa etiqueta de "¡Advertencia!" Para ayudarlos a evitar esta trampa.std::is_constructible<>
anular el rasgo de tipo a menos que sean correctamente SFINAE- restringido, lo que puede no ser trivial para algunos.Primero, déjame corregir algunos detalles. Cuando dices lo siguiente:
Eso es falso. La vinculación a una referencia rvalue no es un movimiento. Solo hay un movimiento.
Además, dado
CreditCard
que no es un parámetro de plantilla,std::forward<CreditCard>(creditCard)
es solo una forma detallada de decirlostd::move(creditCard)
.Ahora...
Si sus tipos tienen movimientos "baratos", es posible que desee hacer su vida más fácil y tomar todo por valor y "
std::move
junto".Este enfoque le dará dos movimientos cuando solo podría producir uno, pero si los movimientos son baratos, pueden ser aceptables.
Mientras estamos en este asunto de "movimientos baratos", debo recordarles que a
std::string
menudo se implementa con la llamada optimización de cadena pequeña, por lo que sus movimientos pueden no ser tan baratos como copiar algunos punteros. Como es habitual con los problemas de optimización, si importa o no es algo para preguntarle a su generador de perfiles, no a mí.¿Qué hacer si no quiere incurrir en esos movimientos adicionales? Tal vez resulten demasiado costosos o, lo que es peor, tal vez los tipos no se puedan mover y podría generar copias adicionales.
Si solo hay un parámetro problemático, puede proporcionar dos sobrecargas, con
T const&
yT&&
. Eso vinculará las referencias todo el tiempo hasta la inicialización real del miembro, donde ocurre una copia o movimiento.Sin embargo, si tiene más de un parámetro, esto conduce a una explosión exponencial en el número de sobrecargas.
Este es un problema que se puede solucionar con un reenvío perfecto. Eso significa que, en su lugar, escribe una plantilla y la usa
std::forward
para llevar la categoría de valor de los argumentos a su destino final como miembros.fuente
Account("",0,{brace, initialisation})
.En primer lugar,
std::string
es un tipo de clase bastante fuerte comostd::vector
. Ciertamente no es primitivo.Si está tomando tipos móviles grandes por valor en un constructor, los incluiría
std::move
en el miembro:Así es exactamente como recomendaría implementar el constructor. Hace que los miembros
number
ycreditCard
se muevan construidos, en lugar de copiarse. Cuando use este constructor, habrá una copia (o movimiento, si es temporal) cuando el objeto se pasa al constructor y luego un movimiento al inicializar el miembro.Ahora consideremos este constructor:
Tienes razón, esto implicará una copia de
creditCard
, porque primero se pasa al constructor por referencia. Pero ahora no puede pasarconst
objetos al constructor (porque la referencia no esconst
) y no puede pasar objetos temporales. Por ejemplo, no podrías hacer esto:Ahora consideremos:
Aquí ha mostrado un malentendido de las referencias de rvalue y
std::forward
. Solo debería usarlostd::forward
cuando el objeto que está reenviando se declara comoT&&
para algún tipo deducidoT
. AquíCreditCard
no se deduce (supongo), por lo questd::forward
se está utilizando por error. Busque referencias universales .fuente
Utilizo una regla bastante simple para el caso general: use copy para POD (int, bool, double, ...) y const & para todo lo demás ...
Y querer copiar o no, no se responde con la firma del método sino más bien con lo que haces con los parámetros.
precisión para puntero: casi nunca los usé. La única ventaja sobre & es que pueden ser nulos o reasignados.
fuente
(*) los punteros pueden referirse a la memoria asignada dinámicamente, por lo tanto, cuando sea posible, debe preferir las referencias a los punteros incluso si, al final, las referencias se implementan generalmente como punteros.
(**) "normalmente" significa por constructor de copia (si pasa un objeto del mismo tipo del parámetro) o por constructor normal (si pasa un tipo compatible para la clase). Cuando pasa un objeto como
myMethod(std::string)
, por ejemplo, el constructor de copia se usará sistd::string
se le pasa un, por lo tanto, debe asegurarse de que exista uno.fuente