LA QUESTION LA PLUS IMPORTANTE D'ABORD :
Existe-t-il des bonnes pratiques sur la façon d'envoyer des paramètres en C++ ? Je trouve que ce n'est pas une mince affaire.
Si votre fonction doit modifier l'objet original transmis, de sorte qu'après le retour de l'appel, les modifications apportées à cet objet seront visibles pour l'appelant, alors vous devez transmettre par référence à la valeur l :
void foo(my_class& obj)
{
// Modify obj here...
}
Si votre fonction n'a pas besoin de modifier l'objet original, ni d'en créer une copie (en d'autres termes, il a seulement besoin d'observer son état), alors vous devez passer par référence à la valeur lval de const
:
void foo(my_class const& obj)
{
// Observe obj here
}
Cela vous permettra d'appeler la fonction à la fois avec des lvalues (les lvalues sont des objets ayant une identité stable) et avec des rvalues (les rvalues sont, par exemple, les objets suivants temporaires ou des objets que vous êtes sur le point de quitter suite à l'appel de std::move()
).
On pourrait également affirmer que pour les types fondamentaux ou les types pour lesquels la copie est rapide comme int
, bool
ou char
il n'est pas nécessaire de passer par référence si la fonction a simplement besoin d'observer la valeur. la transmission par valeur doit être privilégiée . C'est correct si sémantique de référence n'est pas nécessaire, mais que se passe-t-il si la fonction veut stocker un pointeur vers ce même objet d'entrée quelque part, de sorte que les futures lectures de ce pointeur verront les modifications de valeur qui ont été effectuées dans une autre partie du code ? Dans ce cas, le passage par référence est la bonne solution.
Si votre fonction n'a pas besoin de modifier l'objet original, mais doit stocker une copie de cet objet. ( éventuellement pour retourner le résultat d'une transformation de l'entrée sans altérer l'entrée ), vous pouvez alors envisager prise par valeur :
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...
}
L'invocation de la fonction ci-dessus donnera toujours lieu à une copie en cas de passage de lvalues, et à un déplacement en cas de passage de rvalues. Si votre fonction a besoin de stocker cet objet quelque part, vous pouvez effectuer une commande supplémentaire déplacer de celui-ci (par exemple, dans le cas foo()
es une fonction membre qui doit stocker la valeur dans un membre de données ).
En cas de déménagement coûteux pour les objets de type my_class
alors vous pouvez envisager de surcharger foo()
et fournir une version pour les lvalues (acceptant une référence lvalue à const
) et une version pour les rvalues (acceptant une référence 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...
}
Les fonctions ci-dessus sont si semblables, en fait, que vous pourriez en faire une seule fonction : foo()
pourrait devenir une fonction modèle et vous pourriez utiliser une redirection parfaite pour déterminer si un déplacement ou une copie de l'objet transmis sera généré en interne :
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...
}
Vous pouvez en apprendre davantage sur ce modèle en regardant cet exposé de Scott Meyers (tenez compte du fait que le terme " Références universelles " qu'il utilise n'est pas standard).
Une chose à garder à l'esprit est que std::forward
se retrouvera généralement dans un déplacer pour rvalues, donc même si cela semble relativement innocent, faire suivre le même objet plusieurs fois peut être une source de problèmes - par exemple, bouger deux fois du même objet ! Faites donc attention à ne pas mettre cette fonction dans une boucle, et à ne pas transmettre le même argument plusieurs fois dans un appel de fonction :
template<typename C>
void foo(C&& obj)
{
bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous!
}
Notez également que vous ne devez normalement pas recourir à la solution des modèles, sauf si vous avez une bonne raison de le faire, car cela rend votre code plus difficile à lire. Normalement, vous devez vous concentrer sur la clarté et la simplicité. .
Les éléments ci-dessus ne sont que de simples directives, mais la plupart du temps, ils vous orienteront vers de bonnes décisions en matière de conception.
CONCERNANT LE RESTE DE VOTRE MESSAGE :
Si je le réécris comme [...], il y aura 2 mouvements et pas de copie.
Ce n'est pas correct. Pour commencer, une référence rvalue ne peut pas se lier à une lvalue, donc cela ne compilera que si vous passez une rvalue de type CreditCard
à votre constructeur. Par exemple :
// 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));
Mais ça ne marchera pas si vous essayez de faire ça :
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc); // ERROR! cc is an lvalue
Parce que cc
est une lvalue et les références rvalue ne peuvent pas se lier aux lvalues. De plus, lors de la liaison d'une référence à un objet, aucun déplacement n'est effectué il s'agit juste d'une liaison de référence. Ainsi, il n'y aura que des un déplacer.
Ainsi, sur la base des directives fournies dans la première partie de cette réponse, si vous êtes préoccupé par le nombre de coups générés lorsque vous prenez une carte à puce CreditCard
par valeur, vous pourriez définir deux surcharges de constructeur, l'une prenant une référence lvalue à const
( CreditCard const&
) et un autre prenant une référence de valeur r ( CreditCard&&
).
La résolution de surcharge choisira la première option lorsqu'elle passe une valeur l (dans ce cas, une copie sera effectuée) et la seconde lorsqu'elle passe une valeur r (dans ce cas, un déplacement sera effectué).
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
{ }
Votre utilisation de std::forward<>
est normalement vu lorsque vous voulez atteindre une redirection parfaite . Dans ce cas, votre constructeur serait en fait un constructeur modèle et se présenterait plus ou moins comme suit
template<typename C>
Account(std::string number, float amount, C&& creditCard)
: number(number), amount(amount), creditCard(std::forward<C>(creditCard)) { }
Dans un sens, cela combine les deux surcharges que j'ai montrées précédemment en une seule fonction : C
seront déduits pour être CreditCard&
dans le cas où vous passez une lvalue, et en raison des règles d'effondrement des références, cela provoquera l'instanciation de cette fonction :
Account(std::string number, float amount, CreditCard& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard&>(creditCard))
{ }
Cela provoquera un copie-construction de creditCard
comme vous le souhaitez. D'autre part, lorsqu'une valeur r est passée, C
seront déduits pour être CreditCard
et cette fonction sera instanciée à la place :
Account(std::string number, float amount, CreditCard&& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard))
{ }
Cela provoquera un déménagement-construction de creditCard
ce qui est ce que vous voulez (parce que la valeur transmise est une rvalue, et cela signifie que nous sommes autorisés à nous déplacer à partir d'elle).