115 votes

Comment passer les paramètres correctement ?

Je suis un débutant en C++ mais pas un débutant en programmation. J'essaie d'apprendre le C++ (c++11) et la chose la plus importante pour moi n'est pas claire : le passage des paramètres.

J'ai considéré ces exemples simples :

  • Une classe dont tous les membres sont de type primitif :
    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 certain type complexe : Client(std::string firstName, std::string lastName, std::vector<Account> accounts):firstName(firstName), lastName(lastName), accounts(accounts)

Quand je crée un compte, je fais ça :

    CreditCard cc("12345",2,2015,1001);
    Account acc("asdasd",345, cc);

Il est évident que 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 n'y aura qu'un seul exemplaire. 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 mouvements et pas de copie.

Je pense que parfois vous pouvez vouloir copier certains paramètres, 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 bonnes pratiques sur la façon d'envoyer des paramètres en C++, car je trouve cela, disons, pas trivial. Comment traiteriez-vous mes exemples présentés ci-dessus ?

165voto

Andy Prowl Points 62121

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).

11voto

D'abord, laissez-moi corriger quelques détails. Quand vous dites ce qui suit :

il y aura 2 déplacements et aucune copie.

C'est faux. Se lier à une référence rvalue n'est pas un déplacement. Il n'y a qu'un seul déplacement.

En outre, étant donné que CreditCard n'est pas un paramètre de modèle, std::forward<CreditCard>(creditCard) est juste une façon verbeuse de dire std::move(creditCard) .

Maintenant...

Si vos types ont des mouvements "bon marché", vous pouvez vous simplifier la vie et prendre tout par valeur et " std::move le long".

Account(std::string number, float amount, CreditCard creditCard)
: number(std::move(number),
  amount(amount),
  creditCard(std::move(creditCard)) {}

Cette approche vous donnera deux coups alors qu'elle ne pourrait en donner qu'un, mais si les coups sont bon marché, ils peuvent être acceptables.

Tant que nous sommes sur cette question des "coups bas", je dois vous rappeler que std::string est souvent implémenté avec la soi-disant petite optimisation de chaîne, donc ses déplacements peuvent ne pas être aussi bon marché que la copie de certains pointeurs. Comme d'habitude avec les questions d'optimisation, c'est à votre profileur qu'il faut demander si cela est important ou non, pas à moi.

Que faire si vous ne voulez pas subir ces déplacements supplémentaires ? Peut-être sont-ils trop chers, ou pire, peut-être les types ne peuvent-ils pas être déplacés et vous risquez de devoir payer des copies supplémentaires.

S'il n'y a qu'un seul paramètre problématique, vous pouvez fournir deux surcharges, avec T const& y T&& . 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 par une redirection 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.

template <typename TString, typename TCreditCard>
Account(TString&& number, float amount, TCreditCard&& creditCard)
: number(std::forward<TString>(number),
  amount(amount),
  creditCard(std::forward<TCreditCard>(creditCard)) {}

6voto

Joseph Mansfield Points 59346

Tout d'abord, std::string est un type de classe assez lourd tout comme std::vector . Ce n'est certainement pas primitif.

Si vous prenez des grands types mobiles par valeur dans un constructeur, je voudrais std::move dans le membre :

CreditCard(std::string number, float amount, CreditCard creditCard)
  : number(std::move(number)), amount(amount), creditCard(std::move(creditCard))
{ }

C'est exactement la façon dont je recommanderais d'implémenter le constructeur. Il fait en sorte que les membres number y creditCard pour être construit en mouvement, plutôt qu'en copie. Lorsque vous utilisez ce constructeur, il y aura une copie (ou un déplacement, s'il est temporaire) lorsque l'objet est transmis au constructeur, puis un déplacement lors de l'initialisation du membre.

Considérons maintenant ce constructeur :

Account(std::string number, float amount, CreditCard& creditCard)
  : number(number), amount(amount), creditCard(creditCard)

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 const au constructeur (parce que la référence est non const ) et vous ne pouvez pas passer des objets temporaires. Par exemple, vous ne pourriez pas faire ceci :

Account account("something", 10.0f, CreditCard("12345",2,2015,1001));

Considérons maintenant :

Account(std::string number, float amount, CreditCard&& creditCard)
  : number(number), amount(amount), creditCard(std::forward<CreditCard>(creditCard))

Ici, vous avez montré une mauvaise compréhension des références rvalue et std::forward . Vous ne devriez vraiment utiliser que std::forward lorsque l'objet que vous transférez est déclaré comme étant T&& pour certains type déduit T . Ici CreditCard n'est pas déduit (je suppose), et donc la fonction std::forward est utilisé par erreur. Regardez en haut références universelles .

1voto

Jefffrey Points 31698

La chose la plus importante n'est pas claire pour moi : le passage des paramètres.

  • Si vous voulez modifier la variable passée à l'intérieur de la fonction / méthode
    • vous le passez par référence
    • vous le passez comme un pointeur (*)
  • Si vous voulez lire la valeur / variable passée à l'intérieur de la fonction / méthode
    • vous le passez par référence constante
  • Si vous voulez modifier la valeur passée à l'intérieur de la fonction / méthode
    • vous le passez normalement en copiant l'objet (**)

(*) Les pointeurs peuvent faire référence à de la mémoire allouée dynamiquement. Par conséquent, lorsque cela est possible, vous devez préférer les références aux pointeurs, même si les références sont, en fin de compte, généralement mises en œuvre comme des pointeurs.

(**) "normalement" signifie par constructeur de copie (si vous passez un objet du même type que le paramètre) ou par constructeur normal (si vous passez un type compatible pour la classe). Lorsque vous passez un objet en tant que myMethod(std::string) Par exemple, le constructeur de copie sera utilisé si un fichier de type std::string lui est transmise, vous devez donc vous assurer qu'il en existe une.

1voto

David Fleury Points 46

J'utilise une règle assez simple pour le cas général : Utiliser la copie pour les POD (int, bool, double,...) et const & pour tout le reste...

Et le fait de vouloir copier ou non, n'est pas répondu par la signature de la méthode mais plus par ce que vous faites avec les paramètres.

struct A {
  A(const std::string& aValue, const std::string& another) 
    : copiedValue(aValue), justARef(another) {}
  std::string copiedValue;
  const std::string& justARef; 
};

précision pour le pointeur : je ne les ai presque jamais utilisés. Le seul avantage sur & est qu'ils peuvent être nuls, ou réassignés.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X