Je suis un débutant en C ++ mais pas un débutant en programmation. J'essaie d'apprendre le C ++ (c ++ 11) et ce n'est pas clair pour moi la chose la plus importante: passer des paramètres.
J'ai considéré ces exemples simples:
Une classe qui a tous ses membres types primitifs:
CreditCard(std::string number, int expMonth, int expYear,int pin):number(number), expMonth(expMonth), expYear(expYear), pin(pin)
Une classe qui a comme membres des types primitifs + 1 type complexe:
Account(std::string number, float amount, CreditCard creditCard) : number(number), amount(amount), creditCard(creditCard)
Une classe qui a comme membres des types primitifs + 1 collection d'un type complexe:
Client(std::string firstName, std::string lastName, std::vector<Account> accounts):firstName(firstName), lastName(lastName), accounts(accounts)
Lorsque je crée un compte, je fais ceci:
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc);
Évidemment, la carte de crédit sera copiée deux fois dans ce scénario. Si je réécris ce constructeur comme
Account(std::string number, float amount, CreditCard& creditCard)
: number(number)
, amount(amount)
, creditCard(creditCard)
il y en aura une copie. Si je le réécris comme
Account(std::string number, float amount, CreditCard&& creditCard)
: number(number)
, amount(amount)
, creditCard(std::forward<CreditCard>(creditCard))
Il y aura 2 coups et aucune copie.
Je pense que parfois vous voudrez peut-être copier un paramètre, parfois vous ne voulez pas copier lorsque vous créez cet objet.
Je viens de C # et, étant habitué aux références, c'est un peu étrange pour moi et je pense qu'il devrait y avoir 2 surcharges pour chaque paramètre mais je sais que je me trompe.
Existe-t-il des meilleures pratiques pour envoyer des paramètres en C ++ parce que je le trouve vraiment, disons, pas trivial. Comment géreriez-vous mes exemples présentés ci-dessus?
std::string
est une classe, tout commeCreditCard
, pas un type primitif."abc"
n'est pas de typestd::string
, pas de typechar */const char *
, mais de typeconst char[N]
(avec N = 4 dans ce cas en raison de trois caractères et d'un nul). C'est une bonne idée fausse courante de se soustraire.Réponses:
LA QUESTION LA PLUS IMPORTANTE D'ABORD:
Si votre fonction a besoin de modifier l'objet d'origine passé, de sorte qu'après le retour de l'appel, les modifications apportées à cet objet soient visibles pour l'appelant, alors vous devez passer par la référence lvalue :
Si votre fonction n'a pas besoin de modifier l'objet d'origine, et n'a pas besoin d'en créer une copie (en d'autres termes, il suffit d'observer son état), alors vous devez passer par référence lvalue à
const
:Cela vous permettra d'appeler la fonction à la fois avec des lvalues (les lvalues sont des objets avec une identité stable) et avec des rvalues (les rvalues sont, par exemple, des temporaires ou des objets dont vous êtes sur le point de vous déplacer suite à un appel
std::move()
).On pourrait également soutenir que pour les types fondamentaux ou les types pour lesquels la copie est rapide , tels que
int
,,bool
ouchar
, il n'est pas nécessaire de passer par référence si la fonction a simplement besoin d'observer la valeur, et le passage par valeur doit être privilégié . C'est correct si la sémantique de référence n'est pas nécessaire, mais que se passerait-il si la fonction voulait stocker un pointeur vers ce même objet d'entrée quelque part, de sorte que les futures lectures de ce pointeur voient 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 d'origine, mais doit stocker une copie de cet objet ( éventuellement pour renvoyer le résultat d'une transformation de l'entrée sans modifier l'entrée ), alors vous pouvez envisager de prendre par valeur :
L'appel de la fonction ci-dessus entraînera toujours une copie lors du passage de lvalues, et un déplacement lors du passage de rvalues. Si votre fonction a besoin de stocker cet objet quelque part, vous pouvez effectuer un déplacement supplémentaire à partir de celui-ci (par exemple, dans le cas où il
foo()
s'agit d' une fonction membre qui doit stocker la valeur dans un membre de données ).Dans le cas où les déplacements sont coûteux pour les objets de type
my_class
, alors vous pouvez envisager de surchargerfoo()
et fournir une version pour lvalues (acceptant une référence lvalue àconst
) et une version pour rvalues (acceptant une référence rvalue):Les fonctions ci-dessus sont si similaires, en fait, que vous pourriez en faire une seule fonction:
foo()
pourrait devenir un modèle de fonction et vous pourriez utiliser un transfert parfait pour déterminer si un déplacement ou une copie de l'objet passé sera généré en interne:Vous voudrez peut-être en savoir plus sur cette conception en regardant cette conférence de Scott Meyers (faites attention au fait que le terme « références universelles » qu'il utilise n'est pas standard).
Une chose à garder à l'esprit est que
std::forward
cela se terminera généralement par un mouvement pour les valeurs, donc même si cela semble relativement innocent, transmettre le même objet plusieurs fois peut être une source de problèmes - par exemple, passer deux fois du même objet! Veillez donc à ne pas mettre cela en boucle, et à ne pas transmettre le même argument plusieurs fois dans un appel de fonction:Notez également que vous ne recourez normalement pas à la solution basée sur un modèle, sauf si vous avez une bonne raison, car cela rend votre code plus difficile à lire. Normalement, vous devez vous concentrer sur la clarté et la simplicité .
Ce ne sont que de simples directives, mais la plupart du temps, elles vous orienteront vers de bonnes décisions de conception.
CONCERNANT LE RESTE DE VOTRE POSTE:
Ce n'est pas correct. Pour commencer, une référence rvalue ne peut pas se lier à une lvalue, donc cela ne sera compilé que lorsque vous passerez une rvalue de type
CreditCard
à votre constructeur. Par exemple:Mais cela ne fonctionnera pas si vous essayez de faire ceci:
Car
cc
is an lvalue et les références rvalue ne peuvent pas se lier à lvalues. De plus, lors de la liaison d'une référence à un objet, aucun déplacement n'est effectué : c'est juste une liaison de référence. Ainsi, il n'y aura qu'un seul mouvement.Donc, sur la base des instructions fournies dans la première partie de cette réponse, si vous êtes préoccupé par le nombre de mouvements générés lorsque vous prenez une
CreditCard
valeur par, vous pouvez définir deux surcharges de constructeur, l'une prenant une référence lvalue àconst
(CreditCard const&
) et l'autre prenant une référence rvalue (CreditCard&&
).La résolution de surcharge sélectionnera le premier lors du passage d'une lvalue (dans ce cas, une copie sera effectuée) et le dernier lors du passage d'une rvalue (dans ce cas, un déplacement sera effectué).
Votre utilisation de
std::forward<>
est normalement visible lorsque vous souhaitez obtenir une transmission parfaite . Dans ce cas, votre constructeur serait en fait un modèle de constructeur , et ressemblerait plus ou moins comme suitDans un sens, cela combine les deux surcharges que j'ai montrées précédemment en une seule fonction:
C
sera déduit auCreditCard&
cas où vous passeriez une lvalue, et en raison des règles de réduction de référence, cela provoquera l'instanciation de cette fonction:Cela entraînera une copie-construction de
creditCard
, comme vous le souhaiteriez. D'autre part, lorsqu'une rvalue est passée,C
elle sera déduite comme étantCreditCard
, et cette fonction sera instanciée à la place:Cela entraînera une construction de déplacement de
creditCard
, ce que vous voulez (car la valeur passée est une rvalue, et cela signifie que nous sommes autorisés à en sortir).la source
const
. Si vous avez besoin de modifier l'objet d'origine, prenez par ref à non-`const. Si vous avez besoin de faire une copie et que les déplacements sont bon marché, prenez par valeur puis déplacez-vous.obj
au lieu de faire une copie locale dans laquelle vous déplacer, non?std::forward
peut être appelé qu'une seule fois . J'ai vu des gens le mettre en boucle, etc. et puisque cette réponse sera vue par beaucoup de débutants, il devrait à mon humble avis y avoir une grosse étiquette «Attention!» Pour les aider à éviter ce piège.std::is_constructible<>
trait de type à moins qu'ils ne soient correctement SFINAE- contraint - ce qui n'est peut-être pas anodin pour certains.Tout d'abord, permettez-moi de corriger certains détails. Quand vous dites ce qui suit:
C'est faux. La liaison à une référence rvalue n'est pas un déplacement. Il n'y a qu'un seul mouvement.
De plus, puisque
CreditCard
n'est pas un paramètre de modèle,std::forward<CreditCard>(creditCard)
c'est juste une façon verbeuse de direstd::move(creditCard)
.Maintenant...
Si vos types ont des mouvements "bon marché", vous voudrez peut-être simplement vous simplifier la vie et prendre tout par valeur et "
std::move
avec".Cette approche vous donnera deux coups alors qu'elle ne pourrait en donner qu'un, mais si les mouvements sont bon marché, ils peuvent être acceptables.
Pendant que nous sommes sur cette question de "coups bon marché", je dois vous rappeler que cela
std::string
est souvent implémenté avec l'optimisation dite des petites chaînes, donc ses mouvements peuvent ne pas être aussi bon marché que la copie de certains pointeurs. Comme d'habitude avec les problèmes d'optimisation, que ce soit important ou non, c'est quelque chose à demander à votre profileur, pas à moi.Que faire si vous ne voulez pas subir ces mouvements supplémentaires? Peut-être qu'ils s'avèrent trop chers, ou pire, peut-être que les types ne peuvent pas être déplacés et que vous risquez d'encourir des copies supplémentaires.
S'il n'y a qu'un seul paramètre problématique, vous pouvez fournir deux surcharges, avec
T const&
etT&&
. Cela liera les références tout le temps jusqu'à l'initialisation réelle du membre, où une copie ou un déplacement se produit.Cependant, si vous avez plus d'un paramètre, cela conduit à une explosion exponentielle du nombre de surcharges.
C'est un problème qui peut être résolu avec une transmission parfaite. Cela signifie que vous écrivez un modèle à la place et que vous utilisez
std::forward
pour transporter la catégorie de valeur des arguments jusqu'à leur destination finale en tant que membres.la source
Account("",0,{brace, initialisation})
.Tout d'abord,
std::string
c'est un type de classe assez lourd, tout commestd::vector
. Ce n'est certainement pas primitif.Si vous prenez des types mobiles volumineux par valeur dans un constructeur, je les ferais
std::move
dans le membre:C'est exactement ainsi que je recommanderais d'implémenter le constructeur. Cela provoque la construction des membres
number
etcreditCard
du déplacement, plutôt que la construction par copie. Lorsque vous utilisez ce constructeur, il y aura une copie (ou un déplacement, s'il est temporaire) lorsque l'objet est passé au constructeur, puis un déplacement lors de l'initialisation du membre.Considérons maintenant ce constructeur:
Vous avez raison, cela impliquera une copie de
creditCard
, car il est d'abord passé au constructeur par référence. Mais maintenant, vous ne pouvez pas passer d'const
objets au constructeur (car la référence est non-const
) et vous ne pouvez pas passer d'objets temporaires. Par exemple, vous ne pouvez pas faire ceci:Considérons maintenant:
Ici, vous avez montré une incompréhension des références rvalue et
std::forward
. Vous ne devriez vraiment l'utiliser questd::forward
lorsque l'objet que vous transférez est déclaré commeT&&
pour un type déduitT
. IciCreditCard
n'est pas déduit (je suppose), et donc lestd::forward
est utilisé par erreur. Recherchez des références universelles .la source
J'utilise une règle assez simple pour le cas général: utilisez la copie pour POD (int, bool, double, ...) et const & pour tout le reste ...
Et vouloir copier ou pas, ne répond pas par la signature de la méthode mais plus par ce que vous faites avec les paramètres.
précision pour pointeur: je ne les ai presque jamais utilisés. Le seul avantage par rapport aux & est qu'ils peuvent être nuls ou réaffectés.
la source
(*) les pointeurs peuvent faire référence à de la mémoire allouée dynamiquement, donc lorsque cela est possible, vous devriez préférer les références aux pointeurs même si les références sont, en fin de compte, généralement implémentées comme des pointeurs.
(**) "normalement" signifie par constructeur de copie (si vous passez un objet du même type de paramètre) ou par constructeur normal (si vous passez un type compatible pour la classe). Lorsque vous passez un objet comme
myMethod(std::string)
, par exemple, le constructeur de copie sera utilisé si un luistd::string
est passé, vous devez donc vous assurer qu'il en existe un.la source