LA DOMANDA PIÙ IMPORTANTE PRIMA:
Esistono buone pratiche su come inviare parametri in C ++ perché lo trovo davvero, diciamo, non banale
Se la tua funzione deve modificare l'oggetto originale passato, in modo che dopo il ritorno della chiamata, le modifiche a quell'oggetto saranno visibili al chiamante, dovresti passare per riferimento lvalue :
void foo(my_class& obj)
{
// Modify obj here...
}
Se la tua funzione non ha bisogno di modificare l'oggetto originale e non ha bisogno di crearne una copia (in altre parole, deve solo osservare il suo stato), allora dovresti passare per riferimento lvalue aconst
:
void foo(my_class const& obj)
{
// Observe obj here
}
Ciò ti consentirà di chiamare la funzione sia con lvalues (lvalues sono oggetti con un'identità stabile) che con rvalues (rvalues sono, ad esempio , temporanei , o oggetti da cui stai per spostarti come risultato della chiamata std::move()
).
Si potrebbe anche sostenere che per tipi fondamentali o tipi per i quali la copia è veloce , come int
, bool
o char
, non è necessario passare per riferimento se la funzione deve semplicemente osservare il valore e il passaggio per valore dovrebbe essere favorito . Questo è corretto se la semantica di riferimento non è necessaria, ma cosa succede se la funzione volesse memorizzare un puntatore a quello stesso oggetto di input da qualche parte, in modo che le future letture attraverso quel puntatore vedranno le modifiche al valore che sono state eseguite in qualche altra parte del codice? In questo caso, passare per riferimento è la soluzione corretta.
Se la tua funzione non ha bisogno di modificare l'oggetto originale, ma ha bisogno di memorizzare una copia di quell'oggetto ( possibilmente per restituire il risultato di una trasformazione dell'input senza alterare l'input ), allora potresti considerare di prendere per valore :
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...
}
Invocare la funzione di cui sopra risulterà sempre in una copia quando si passano i valori l e in una si sposta quando si passano i valori. Se la tua funzione ha bisogno di memorizzare questo oggetto da qualche parte, puoi eseguire uno spostamento aggiuntivo da esso (ad esempio, nel caso in cui foo()
è una funzione membro che deve memorizzare il valore in un membro dati ).
Nel caso in cui le mosse siano costose per oggetti di tipo my_class
, puoi prendere in considerazione il sovraccarico foo()
e fornire una versione per lvalues (accettando un riferimento lvalue a const
) e una versione per rvalues (accettando un riferimento 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...
}
Le funzioni di cui sopra sono così simili, infatti, che potresti ricavarne una singola funzione: foo()
potrebbe diventare un modello di funzione e potresti utilizzare l' inoltro perfetto per determinare se verrà generata internamente una mossa o una copia dell'oggetto passato:
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...
}
Potresti voler saperne di più su questo progetto guardando questo discorso di Scott Meyers ( fai attenzione al fatto che il termine " Riferimenti universali " che sta usando non è standard).
Una cosa da tenere a mente è che di std::forward
solito finirà in una mossa per rvalues, quindi anche se sembra relativamente innocente, inoltrare lo stesso oggetto più volte può essere fonte di problemi, ad esempio spostarsi dallo stesso oggetto due volte! Quindi fai attenzione a non metterlo in un ciclo e non inoltrare lo stesso argomento più volte in una chiamata di funzione:
template<typename C>
void foo(C&& obj)
{
bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous!
}
Si noti inoltre che normalmente non si ricorre alla soluzione basata su modelli a meno che non si abbia una buona ragione per farlo, poiché rende il codice più difficile da leggere. Normalmente, dovresti concentrarti sulla chiarezza e sulla semplicità .
Quanto sopra sono solo semplici linee guida, ma la maggior parte delle volte ti indirizzeranno verso buone decisioni di progettazione.
RIGUARDO AL RESTO DEL TUO POST:
Se lo riscrivo come [...] ci saranno 2 mosse e nessuna copia.
Questo non è corretto. Per cominciare, un riferimento rvalue non può legarsi a un lvalue, quindi verrà compilato solo quando si passa un rvalue di tipo CreditCard
al costruttore. Per esempio:
// 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));
Ma non funzionerà se provi a farlo:
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc); // ERROR! cc is an lvalue
Perché cc
è un lvalue e i riferimenti rvalue non possono legarsi a lvalues. Inoltre, quando si associa un riferimento a un oggetto, non viene eseguita alcuna mossa : è solo un'associazione di riferimento. Quindi, ci sarà solo una mossa.
Quindi, in base alle linee guida fornite nella prima parte di questa risposta, se sei interessato al numero di mosse generate quando prendi un CreditCard
valore per, puoi definire due overload del costruttore, uno che prende un riferimento lvalue a const
( CreditCard const&
) e uno che prende un riferimento rvalue ( CreditCard&&
).
La risoluzione del sovraccarico selezionerà il primo al passaggio di un valore (in questo caso verrà eseguita una copia) e il secondo al passaggio di un valore (in questo caso verrà eseguita una mossa).
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
{ }
L'utilizzo di std::forward<>
viene normalmente visualizzato quando si desidera ottenere un inoltro perfetto . In tal caso, il tuo costruttore sarebbe effettivamente un modello di costruttore e apparirebbe più o meno come segue
template<typename C>
Account(std::string number, float amount, C&& creditCard)
: number(number), amount(amount), creditCard(std::forward<C>(creditCard)) { }
In un certo senso, questo combina entrambi i sovraccarichi che ho mostrato in precedenza in un'unica funzione: C
verrà dedotto come CreditCard&
nel caso in cui stai passando un lvalue e, a causa delle regole di collassamento dei riferimenti, causerà l'istanza di questa funzione:
Account(std::string number, float amount, CreditCard& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard&>(creditCard))
{ }
Ciò causerà una copia-costruzione di creditCard
, come vorresti. D'altra parte, quando viene passato un rvalue, C
verrà dedotto essere CreditCard
, e questa funzione verrà invece istanziata:
Account(std::string number, float amount, CreditCard&& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard))
{ }
Ciò causerà una costruzione del movimento di creditCard
, che è ciò che vuoi (perché il valore passato è un rvalue, e questo significa che siamo autorizzati a muoverci da esso).