89 votes

Passer par valeur vs passer par référence rvalue

Quand dois-je déclarer ma fonction en tant que:

void foo(Widget w);

par opposition à

void foo(Widget&& w);?

Suppose que c'est le seul cas de surcharge (comme dans, je choisis l'un ou l'autre, pas les deux, et pas d'autres surcharges). Aucun des modèles impliqués. Supposons que la fonction foo nécessite l'appropriation de l' 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'après pour pourquoi ces contraintes font partie de la question.

La principale différence que mes collègues et je peux trouver, c'est que la référence rvalue paramètre vous oblige à être explicite sur les copies. L'appelant est responsable de faire une copie explicite, puis en le passant avec std::move lorsque vous voulez une copie. Dans le pass en cas de valeur, le coût de la copie est caché:

    //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 celui de la différence. Autre que de forcer les gens à être explicite sur la copie et la modification de la façon dont beaucoup de sucre syntaxique que vous obtenez lors de l'appel de la fonction, sinon, comment sont-ils différents? Que disent-ils différemment à propos de l'interface? Sont-ils plus ou moins efficace qu'un autre?

D'autres choses que mes collègues et j'ai déjà pensé à:

  • La référence rvalue paramètre signifie que vous pouvez déplacer l'argument, mais n'a pas le mandat. Il est possible que l'argument que vous avez passé à l'appel site sera dans son état d'origine par la suite. Il est également possible que la fonction serait de manger/changer l'argument sans jamais appeler un constructeur de déplacement mais supposons que parce que c'était une référence rvalue, l'appelant a renoncé à un contrôle. Le passage par valeur, si vous avancez, vous devez supposer qu'un déménagement est arrivé; il n'y a pas le choix.
  • En supposant qu'aucun elisions, d'un seul mouvement appel du constructeur est éliminé avec passer par rvalue.
  • Le compilateur a plus de chance d'éluder copies/se déplace avec le passage par valeur. Quelqu'un peut-il justifier cette revendication? De préférence avec un lien vers gcc.godbolt.org montrant optimisé le code généré à partir de gcc/clang plutôt que d'une ligne dans la norme. Ma tentative de montrer ce n'était probablement pas réussi à isoler le problème: https://godbolt.org/g/4yomtt

Addendum: pourquoi suis-je contraindre ce problème tellement?

  • Pas de surcharges - si il y avait d'autres surcharges, ce serait avilir dans une discussion de passage par valeur par rapport à un ensemble de surcharges qui comprennent à la fois const référence et référence rvalue, à quel point l'ensemble de surcharges est évidemment plus efficace et gagne. C'est bien connu, et n'est donc pas intéressant.
  • Pas de modèles - je ne suis pas intéressé à savoir comment le transfert des références d'être dans la photo. Si vous avez un transfert de référence, vous faites appel à std::forward de toute façon. L'objectif avec un transfert de référence est de passer à des choses que vous avez reçus. Les Copies ne sont pas pertinentes parce que vous venez de passer une lvalue à la place. C'est bien connu, et pas du tout intéressant.
  • foo requiert l'acquisition d' Widget (aka n' const Widget&) - Nous ne parlons pas en lecture seule les fonctions. Si la fonction était en lecture seule, ou n'ont pas besoin de posséder ou de prolonger la durée de vie de l' Widget, alors la réponse trivialement devient const Widget&, ce qui encore une fois, c'est bien connu, et pas du tout intéressant. Je vous renvois aussi pourquoi nous ne voulons pas parler de surcharges.

62voto

Altainia Points 482

Ce n'rvalue usages dire sur une interface contre la copie? rvalue l'indique à l'appelant que la fonction des deux veut posséder la valeur et n'a pas l'intention de laisser savoir à la cliente de tous les changements qu'il a fait. Considérons la suite (je sais que vous n'en dit pas lvalue références dans votre exemple, mais garder avec moi):

//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 réelle des widgets dans, disons, GTK faire), puis la première option ne peut pas travailler. Les deuxième, troisième et quatrième options ont un sens, car il n'y a toujours qu'un seul véritable représentation des données. De toute façon, c'est ce que ces sémantique de me dire quand je les vois dans le code.

Maintenant, que de l'efficacité: il dépend. références rvalue permet d'économiser beaucoup de temps si le Widget a un pointeur sur un membre de données dont l'a indiqué à la table des matières peut être assez grande (penser à un tableau). Depuis l'appelant a utilisé une rvalue, ils disent qu'ils ne se soucient pas de ce qu'ils donnent plus de vous. Donc, si vous voulez aller de l'appelant du Widget contenu dans votre Widget, il suffit de prendre leur pointeur. Pas besoin d'méticuleusement copie de chaque élément dans la structure de données de leurs pointeur pointe. Cela peut conduire à d'assez bonnes améliorations dans la vitesse (encore une fois, penser à des tableaux). Mais si le Widget de la classe n'a pas une telle chose, cette prestation est nulle part pour être vu.

J'espère que ça devient à ce que vous demandez; si non, je peux peut-être élargir ou préciser les choses.

19voto

Jarod42 Points 15729

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

Oui, passer par rvalue-référence obtenu un point.

La référence rvalue paramètre signifie que vous pouvez déplacer l'argument, mais n'a pas le mandat.

Oui, passe-par-valeur a obtenu un point. Mais qui donnent aussi de passer par rvalue de la possibilité de gérer exception de garantie: si foo lancers, widget de la valeur n'est pas nécessaire consommé.

Pour déplacer uniquement les types std::unique_ptr), passer-par-valeur semble être la norme (surtout 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'une des shared_ptrs'constructeur std::unique_ptr<T, D>&&.

Pour les types qui possède à 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 force d'être explicite sur la copie.

Sauf si vous voulez de la garantie, il n'est pas indésirable copie, je voudrais utiliser des passe-par-valeur de cohérence. Sauf si vous voulez de la garantie et/ou immédiate de l'évier, je voudrais utiliser des passe-par-rvalue.

Pour une base de code existante, j'aimerais garder la cohérence.

15voto

Chris Drew Points 1743

À moins que le type est un déplacer uniquement de type normalement, vous avez l'option de passer par référence à const et il semble arbitraire de le faire "ne fait pas partie de la discussion", mais je vais essayer.

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

La fonction a besoin d'une copie locale

Disons - Widget est un itérateur et vous souhaitez mettre en place votre propre std::next fonction. next des besoins de sa propre copie à l'avance et de retour. Dans ce cas, votre choix est quelque chose comme:

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

vs

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

Je pense que la valeur est mieux ici. À partir de la signature que vous pouvez le voir il est de prendre une copie. Si l'appelant veut éviter un copie qu'ils peuvent faire un std::move et la garantie de la variable est déplacé, mais ils peuvent encore passer lvalues si ils le veulent. Avec passe-par-rvalue-référence de l'appelant ne peut pas garantir que la variable a été déplacé de.

Déplacez-affectation à une copie

Disons que vous avez une classe WidgetHolder:

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

et vous avez besoin de mettre en œuvre un setWidget de la fonction membre. 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 la mesure de la performance que vous décidez que vous avez besoin pour optimiser la r-valeurs. Vous avez le choix entre le remplacement avec:

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

Ou de surcharge avec:

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

Celui-ci est un peu plus délicat. Il est tentant de choisir de passer par valeur, parce qu'il accepte à la fois les rvalues et lvalues de sorte que vous n'avez pas besoin de deux surcharges. Toutefois, il est sans condition de prendre une copie de sorte que vous ne pouvez pas profiter de toutes les capacités existantes dans la variable membre. Le passage par référence à const et de passer par la r-valeur de référence des surcharges d'utilisation d'affectation sans prendre une copie de ce qui pourrait être plus rapide

Déplacez-la construction d'une copie

Maintenant, disons que vous écrivez le constructeur WidgetHolder et comme avant, vous avez déjà mis en œuvre un constructeur qui prend une référence à const:

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

et comme avant, vous avez mesuré la performance et a décidé de vous avez besoin pour optimiser les rvalues. Vous avez le choix entre le remplacement avec:

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

Ou de surcharge avec:

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

Dans ce cas, la variable de membre ne peut pas avoir la capacité existante puisque c'est le constructeur. Vous déplacez-constucting une copie. Aussi, les constructeurs prennent souvent beaucoup de paramètres de sorte qu'il peut être une douleur à écrire toutes les permutations différentes surcharges afin d'optimiser la valeur r des références. 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.

En passant unique_ptr

Avec unique_ptr l'efficacité n'est moins important, étant donné qu'un déménagement n'est vraiment pas cher et il n'a pas de capacité. Le plus important est d'expressivité et de l'exactitude. Il y a une bonne discussion de comment passer unique_ptr ici.

7voto

ruakh Points 68789

Une question qui n'est pas mentionné dans les autres réponses, c'est l'idée de l'exception de sécurité.

En général, si la fonction renvoie une exception, nous l'idéal serait d'avoir la forte exception de garantie, ce qui signifie que l'appel n'a aucun effet autre que de lever une exception. Si le laisser-passer-par-valeur utilise le constructeur de déplacement, alors un tel effet est essentiellement inévitable. Donc une rvalue-référence argument peut être supérieure dans certains cas. (Bien sûr, il y a plusieurs cas où la forte exception de garantie n'est pas réalisable ou l'autre manière, ainsi que les divers cas où le pas de jet de garantie est valable dans les deux sens. Ce n'est donc pas pertinente dans 100% des cas. Mais elle est pertinente, parfois.)

6voto

Nick Points 142

Quand vous passez par référence rvalue objet de la durée de vie se complique. Si le destinataire de l'appel ne se déplace pas hors de l'argument, la destruction de l'argument est retardée. Je pense que ce qui est intéressant dans les deux cas.

Tout d'abord, vous avez un RAII classe

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 pourrait encore être détenus par x si fn ne se déplace pas en dehors de la référence rvalue. Dans le passage de la valeur de code, nous savons qu' x est déplacé hors de, et fn communiqués x. C'est probablement un cas où vous souhaitez passer par valeur, et le constructeur de copie serait susceptible d'être supprimé, de sorte que vous n'avez pas à vous inquiéter accidentelle des copies.

Deuxièmement, si l'argument est un objet de grande taille et la fonction ne se déplace pas, la durée de vie des vecteurs de données est plus importante que dans le cas d'un 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 pas sortir de x, alors vous allez vous retrouver avec toutes les données de tous les vecteurs encore en vie. Si vous au lieu de passer par valeur, seul le dernier vecteur de données de l'être encore en vie (en supposant que les vecteurs constructeur de déplacement efface les sources vectorielles).

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