web-dev-qa-db-fra.com

Pass by value vs pass by rvalue reference

Quand dois-je déclarer ma fonction comme:

void foo(Widget w);

par opposition à

void foo(Widget&& w);?

Supposons que c'est la seule surcharge (comme dans, je choisis l'un ou l'autre, pas les deux, et pas d'autres surcharges). Aucun modèle impliqué. Supposons que la fonction foo nécessite la propriété de Widget (par exemple const Widget& ne fait pas partie de cette discussion). Je ne suis pas intéressé par une réponse en dehors de la portée de ces circonstances. Voir l'addendum à la fin de l'article pour pourquoi ces contraintes font partie de la question.

La principale différence que mes collègues et moi pouvons trouver est que le paramètre de référence rvalue vous oblige à être explicite sur les copies. L'appelant est responsable de faire une copie explicite, puis de la transmettre avec std::move lorsque vous voulez une copie. Dans le cas du passage par valeur, le coût de la copie est masqué:

    //If foo is a pass by value function, calling + making a copy:
    Widget x{};
    foo(x); //Implicit copy
    //Not shown: continues to use x locally

    //If foo is a pass by rvalue reference function, calling + making a copy:
    Widget x{};
    //foo(x); //This would be a compiler error
    auto copy = x; //Explicit copy
    foo(std::move(copy));
    //Not shown: continues to use x locally

Autre que cette différence. En plus de forcer les gens à être explicites sur la copie et la modification de la quantité de sucre syntaxique que vous obtenez en appelant la fonction, en quoi sont-ils différents? Que disent-ils différemment de l'interface? Sont-ils plus ou moins efficaces les uns que les autres?

D'autres choses auxquelles mes collègues et moi avons déjà pensé:

  • Le paramètre de référence rvalue signifie que vous mai déplacez l'argument, mais ne le force pas. Il est possible que l'argument que vous avez transmis sur le site d'appel soit dans son état d'origine par la suite. Il est également possible que la fonction mange/modifie l'argument sans jamais appeler de constructeur de déplacement, mais suppose que parce qu'il s'agissait d'une référence rvalue, l'appelant a abandonné le contrôle. Passez par valeur, si vous vous y déplacez, vous devez supposer qu'un mouvement s'est produit; il n'y a pas d'autre choix.
  • En supposant qu'aucune élection, un appel de constructeur de mouvement unique est éliminé avec passage par rvalue.
  • Le compilateur a de meilleures chances d'élider les copies/mouvements avec une valeur de passage. Quelqu'un peut-il étayer cette affirmation? De préférence avec un lien vers gcc.godbolt.org montrant un code généré optimisé à partir de gcc/clang plutôt qu'une ligne dans la norme. Ma tentative de montrer que cela n'a probablement pas réussi à isoler le comportement: https://godbolt.org/g/4yomtt

Addendum: pourquoi suis-je autant contraint à ce problème?

  • Aucune surcharge - s'il y avait d'autres surcharges, cela reviendrait à une discussion de la valeur de passage par rapport à un ensemble de surcharges qui incluent à la fois la référence const et la référence rvalue, auquel cas l'ensemble des surcharges est évidemment plus efficace et gagne. C'est bien connu et donc pas intéressant.
  • Aucun modèle - je ne suis pas intéressé par la façon dont les références de transfert s'intègrent dans l'image. Si vous avez une référence de transfert, vous appelez quand même std :: forward. Le but d'une référence de transfert est de transmettre les choses telles que vous les avez reçues. Les copies ne sont pas pertinentes car vous passez simplement une valeur l à la place. C'est bien connu et pas intéressant.
  • foo nécessite la propriété de Widget (alias non const Widget&) - Nous ne parlons pas de fonctions en lecture seule. Si la fonction était en lecture seule ou n'avait pas besoin de posséder ou d'étendre la durée de vie du Widget, alors la réponse devient trivialement const Widget&, qui, encore une fois, est bien connu et pas intéressant. Je vous renvoie également à la raison pour laquelle nous ne voulons pas parler de surcharges.
47
Mark

Le paramètre de référence rvalue vous oblige à être explicite sur les copies.

Oui, pass-by-rvalue-reference a marqué un point.

Le paramètre de référence rvalue signifie que vous pouvez déplacer l'argument, mais ne le force pas.

Oui, la valeur de passage a un point. Mais cela donne également à pass-by-rvalue la possibilité de gérer la garantie d'exception: si foo lance, widget la valeur n'est pas nécessairement consommée.

Pour les types à déplacement uniquement (comme std::unique_ptr), La valeur de passage semble être la norme (principalement pour votre deuxième point, et le premier point n'est pas applicable de toute façon).

EDIT: la bibliothèque standard contredit ma phrase précédente, l'un des constructeurs de shared_ptr Prend std::unique_ptr<T, D>&&.

Pour les types qui ont à la fois copier/déplacer (comme std::shared_ptr), Nous avons le choix de la cohérence avec les types précédents ou de forcer à être explicite sur la copie.

À moins que vous ne vouliez garantir qu'il n'y a pas de copie indésirable, j'utiliserais le passage par valeur pour la cohérence. À moins que vous ne vouliez un puits garanti et/ou immédiat, j'utiliserais la valeur par passage.

Pour la base de code existante, je garderais la cohérence.

9
Jarod42

Que disent les usages rvalue d'une interface par rapport à la copie? rvalue suggère à l'appelant que la fonction veut à la fois posséder la valeur et n'a pas l'intention de faire savoir à l'appelant les changements qu'elle a apportés. Considérez ce qui suit (je sais que vous avez dit qu'il n'y avait pas de références de valeur dans votre exemple, mais soyez indulgent)

//Hello. I want my own local copy of your Widget that I will manipulate,
//but I don't want my changes to affect the one you have. I may or may not
//hold onto it for later, but that's none of your business.
void foo(Widget w);

//Hello. I want to take your Widget and play with it. It may be in a
//different state than when you gave it to me, but it'll still be yours
//when I'm finished. Trust me!
void foo(Widget& w);

//Hello. Can I see that Widget of yours? I don't want to mess with it;
//I just want to check something out on it. Read that one value from it,
//or observe what state it's in. I won't touch it and I won't keep it.
void foo(const Widget& w);

//Hello. Ooh, I like that Widget you have. You're not going to use it
//anymore, are you? Please just give it to me. Thank you! It's my
//responsibility now, so don't worry about it anymore, m'kay?
void foo(Widget&& w);

Pour une autre façon de voir les choses:

//Here, let me buy you a new car just like mine. I don't care if you wreck
//it or give it a new Paint job; you have yours and I have mine.
void foo(Car c);

//Here are the keys to my car. I understand that it may come back...
//not quite the same... as I lent it to you, but I'm okay with that.
void foo(Car& c);

//Here are the keys to my car as long as you promise to not give it a
//Paint job or anything like that
void foo(const Car& c);

//I don't need my car anymore, so I'm signing the title over to you now.
//Happy birthday!
void foo(Car&& c);

Maintenant, si les widgets doivent rester uniques (comme les widgets réels dans, disons, GTK), la première option ne peut pas fonctionner. Les deuxième, troisième et quatrième options ont du sens, car il n'y a toujours qu'une seule représentation réelle des données. Quoi qu'il en soit, c'est ce que ces sémantiques me disent quand je les vois dans le code.

Maintenant, quant à l'efficacité: cela dépend. Les références rvalue peuvent gagner beaucoup de temps si Widget a un pointeur vers un membre de données dont le contenu pointé peut être assez volumineux (pensez à un tableau). Puisque l'appelant a utilisé une valeur r, ils disent qu'ils ne se soucient plus de ce qu'ils vous donnent. Donc, si vous souhaitez déplacer le contenu du widget de l'appelant dans votre widget, prenez simplement son pointeur. Pas besoin de copier méticuleusement chaque élément de la structure de données vers lequel pointe son pointeur. Cela peut conduire à de très bonnes améliorations de la vitesse (encore une fois, pensez aux tableaux). Mais si la classe Widget n'a rien de tel, cet avantage est introuvable.

J'espère que cela correspond à ce que vous demandiez; sinon, je peux peut-être développer/clarifier les choses.

10
Altainia

À moins que le type ne soit un mouvement uniquement, vous avez normalement une option pour passer par référence à const et il semble arbitraire de le faire "ne pas faire partie de la discussion" mais j'essaierai.

Je pense que le choix dépend en partie de ce que foo va faire avec le paramètre.

La fonction a besoin d'une copie locale

Disons que Widget est un itérateur et que vous voulez implémenter votre propre std::next fonction. next a besoin de sa propre copie pour avancer puis revenir. Dans ce cas, votre choix est quelque chose comme:

Widget next(Widget it, int n = 1){
    std::advance(it, n);
    return it;
}

contre

Widget next(Widget&& it, int n = 1){
    std::advance(it, n);
    return std::move(it);
}

Je pense que la valeur ajoutée est meilleure ici. De la signature, vous pouvez voir qu'il en prend une copie. Si l'appelant veut éviter une copie, il peut faire un std::move et garantit que la variable est déplacée, mais ils peuvent toujours transmettre des valeurs s'ils le souhaitent. Avec pass-by-rvalue-reference, l'appelant ne peut pas garantir que la variable a été déplacée.

Déplacer l'affectation vers une copie

Disons que vous avez une classe WidgetHolder:

class WidgetHolder {
    Widget widget;
   //...
};

et vous devez implémenter une fonction membre setWidget. Je vais supposer que vous avez déjà une surcharge qui prend une référence à const:

WidgetHolder::setWidget(const Widget& w) {
    widget = w;
}

mais après avoir mesuré les performances, vous décidez que vous devez optimiser les valeurs r. Vous avez le choix entre le remplacer par:

WidgetHolder::setWidget(Widget w) {
    widget = std::move(w);
}

Ou surcharger avec:

WidgetHolder::setWidget(Widget&& widget) {
    widget = std::move(w);
}

Celui-ci est un peu plus délicat. Il est tentant de choisir pass-by-value car il accepte à la fois rvalues ​​et lvalues, vous n'avez donc pas besoin de deux surcharges. Cependant, il prend inconditionnellement une copie, vous ne pouvez donc pas tirer parti de la capacité existante dans la variable membre. Les surcharges de référence par référence à const et de référence par valeur r utilisent l'affectation sans prendre une copie qui pourrait être plus rapide.

Déplacer-construire une copie

Supposons maintenant que vous écrivez le constructeur pour WidgetHolder et comme précédemment, vous avez déjà implémenté un constructeur qui prend une référence à const:

WidgetHolder::WidgetHolder(const Widget& w) : widget(w) {
}

et comme auparavant, vous avez mesuré les performances et décidé que vous devez optimiser les valeurs. Vous avez le choix entre le remplacer par:

WidgetHolder::WidgetHolder(Widget w) : widget(std::move(w)) {
}

Ou surcharger avec:

WidgetHolder::WidgetHolder(Widget&& w) : widget(std:move(w)) {
}

Dans ce cas, la variable membre ne peut avoir aucune capacité existante car il s'agit du constructeur. Vous êtes en train de déplacer une copie. En outre, les constructeurs prennent souvent de nombreux paramètres, il peut donc être très difficile d'écrire toutes les différentes permutations de surcharges pour optimiser les références de valeur r. Donc, dans ce cas, c'est une bonne idée d'utiliser le passage par valeur, surtout si le constructeur prend beaucoup de ces paramètres.

Qui passe unique_ptr

Avec unique_ptr les préoccupations d'efficacité sont moins importantes étant donné qu'un déménagement est si bon marché et qu'il n'a aucune capacité. Plus important est l'expressivité et l'exactitude. Il y a une bonne discussion sur la façon de passer unique_ptrici .

10
Chris Drew

Un problème non mentionné dans les autres réponses est l'idée d'exception-sécurité.

En général, si la fonction lève une exception, nous aimerions idéalement avoir la garantie d'exception forte, ce qui signifie que l'appel n'a d'autre effet que de lever l'exception. Si pass-by-value utilise le constructeur de déplacement, alors un tel effet est essentiellement inévitable. Ainsi, un argument rvalue-reference peut être supérieur dans certains cas. (Bien sûr, il existe divers cas où la garantie d'exception forte n'est pas réalisable de toute façon, ainsi que divers cas où la garantie de non-lancement est disponible de toute façon. Donc, cela n'est pas pertinent dans 100% des cas. Mais c'est pertinent parfois.)

4
ruakh

Lorsque vous passez par rvalue, la durée de vie des objets de référence se complique. Si l'appelé ne quitte pas l'argument, la destruction de l'argument est retardée. Je pense que c'est intéressant dans deux cas.

Tout d'abord, vous avez une classe RAII

void fn(RAII &&);

RAII x{underlying_resource};
fn(std::move(x));
// later in the code
RAII y{underlying_resource};

Lors de l'initialisation de y, la ressource peut toujours être détenue par x si fn ne quitte pas la référence rvalue. Dans le code de passage par valeur, nous savons que x est déplacé et fn libère x. Il s'agit probablement d'un cas où vous voudriez passer par valeur, et le constructeur de copie serait probablement supprimé, vous n'auriez donc pas à vous soucier des copies accidentelles.

Deuxièmement, si l'argument est un grand objet et que la fonction ne se déplace pas, la durée de vie des données des vecteurs est plus longue que dans le cas du passage par valeur.

vector<B> fn1(vector<A> &&x);
vector<C> fn2(vector<B> &&x);

vector<A> va;  // large vector
vector<B> vb = fn1(std::move(va));
vector<C> vc = fn2(std::move(vb));

Dans l'exemple ci-dessus, si fn1 et fn2 ne sortez pas de x, alors vous vous retrouverez avec toutes les données de tous les vecteurs encore en vie. Si vous passez plutôt par valeur, seules les données du dernier vecteur seront toujours vivantes (en supposant que les vecteurs déplacent le constructeur efface le vecteur des sources).

4
Nick

Choisir entre by-value et by-rvalue-ref, sans autres surcharges, n'a pas de sens.

Avec pass by value, l'argument réel peut être une expression lvalue.

Avec pass by rvalue-ref, l'argument réel doit être une rvalue.


Si la fonction stocke une copie de l'argument, alors un choix judicieux est entre pass-by-value et un ensemble de surcharges avec pass-by-ref-to-const et pass-by-rvalue-ref. Pour une expression rvalue comme argument réel, l'ensemble des surcharges peut éviter un mouvement. C'est une décision d'ingénierie, de savoir si la micro-optimisation vaut la complexité et le typage supplémentaires.

2