web-dev-qa-db-fra.com

Quand std :: move doit-il être utilisé sur une valeur de retour de fonction?

Dans ce cas

struct Foo {};
Foo meh() {
  return std::move(Foo());
}

Je suis à peu près sûr que le déménagement est inutile, car le Foo nouvellement créé sera une xvalue.

Mais quoi dans des cas comme ceux-ci?

struct Foo {};
Foo meh() {
  Foo foo;
  //do something, but knowing that foo can safely be disposed of
  //but does the compiler necessarily know it?
  //we may have references/pointers to foo. how could the compiler know?
  return std::move(foo); //so here the move is needed, right?
}

Là le déménagement est nécessaire, je suppose?

108
user2015453

Dans le cas de return std::move(foo);, le move est superflu à cause de 12.8/32:

Lorsque les critères d'élision d'une opération de copie sont remplis ou le seraient, à l'exception du fait que l'objet source est un paramètre de fonction et que l'objet à copier est désigné par une valeur lvalue, la résolution de surcharge pour sélectionner le constructeur de la copie est d'abord effectué comme si l'objet était désigné par une valeur.

return foo; Est un cas d'OVNR, donc la copie est autorisée. foo est une lvalue. Ainsi, le constructeur sélectionné pour la "copie" de foo vers la valeur de retour de meh doit être le constructeur de déplacement, s'il en existe un.

L'ajout de move a toutefois un effet potentiel: il empêche le déplacement d'être supprimé, car return std::move(foo); est non éligible pour un OVNR.

Autant que je sache, 12.8/32 énonce les niquement conditions dans lesquelles une copie d'une valeur lvalue peut être remplacée par un déplacement. En règle générale, le compilateur n'est pas autorisé à détecter qu'une lvalue est inutilisée après la copie (à l'aide de DFA, par exemple) et à effectuer la modification de sa propre initiative. Je suppose ici qu'il y a une différence observable entre les deux - si le comportement observable est le même, la règle "as-if" s'applique.

Donc, pour répondre à la question dans le titre, utilisez std::move Sur une valeur de retour lorsque vous souhaitez que celle-ci soit déplacée et qu'elle ne serait pas déplacée de toute façon. C'est:

  • vous voulez qu'il soit déplacé, et
  • c'est une lvalue, et
  • il n'est pas admissible à la copie, et
  • ce n'est pas le nom d'un paramètre de fonction by-value.

Considérant que ceci est assez délicat et que les mouvements sont généralement économiques, vous voudrez peut-être dire que, dans le code non-template, vous pouvez simplifier un peu cette opération. Utilisez std::move Lorsque:

  • vous voulez qu'il soit déplacé, et
  • c'est une lvalue, et
  • vous ne pouvez pas vous inquiéter à ce sujet.

En suivant les règles simplifiées, vous sacrifiez certaines élisions de mouvement. Pour les types comme std::vector Dont le déplacement est peu coûteux, vous ne le remarquerez probablement jamais (et si vous le remarquez, vous pouvez l'optimiser). Pour les types comme std::array Dont le déplacement coûte cher, ou pour les modèles dans lesquels vous ne savez pas si les déménagements sont économiques ou non, vous en aurez probablement plus à vous inquiéter.

112
Steve Jessop

Le déménagement est inutile dans les deux cas. Dans le second cas, std::move est superflu car vous retournez une variable locale par valeur et le compilateur comprendra que, puisque vous n'utiliserez plus cette variable locale, vous pouvez la déplacer plutôt que la copier.

33
Andy Prowl

Sur une valeur renvoyée, si l'expression return renvoie directement au nom d'une valeur locale (c'est-à-dire à une valeur x), la valeur std::move. D'autre part, si l'expression de retour est pas l'identifiant, il ne sera pas déplacé automatiquement. Ainsi, par exemple, vous auriez besoin de l'explicite std::move dans ce cas:

T foo(bool which) {
   T a = ..., b = ...;
   return std::move(which? a : b);
   // alternatively: return which? std::move(a), std::move(b);
}

Lorsque vous retournez directement une variable locale nommée ou une expression temporaire, évitez le code explicite std::move. Le compilateur doit (et le sera à l'avenir) se déplacer automatiquement dans ces cas, et ajouter std::move pourrait affecter d’autres optimisations.

16

Il y a beaucoup de réponses sur le moment où il ne devrait pas être déplacé, mais la question est "quand devrait-il être déplacé?"

Voici un exemple artificiel du moment où il devrait être utilisé:

std::vector<int> append(std::vector<int>&& v, int x) {
  v.Push_back(x);
  return std::move(v);
}

c'est-à-dire que lorsque vous avez une fonction qui prend une référence rvalue, la modifie et en renvoie une copie. Maintenant, dans la pratique, cette conception est presque toujours meilleure:

std::vector<int> append(std::vector<int> v, int x) {
  v.Push_back(x);
  return v;
}

qui vous permet également de prendre des paramètres non rvalue.

En gros, si vous avez une référence rvalue dans une fonction que vous voulez retourner en déplaçant, vous devez appeler std::move. Si vous avez une variable locale (que ce soit un paramètre ou non), vous la retournerez implicitement moves (et ce déplacement implicite peut être éliminé, alors qu'un déplacement explicite ne peut pas l'être). Si vous avez une fonction ou une opération qui prend des variables locales et renvoie une référence à ladite variable locale, vous devez std::move Pour que le déplacement se produise (à titre d'exemple, l'opérateur trinaire ?:) .

14

Un compilateur C++ est libre d'utiliser std::move(foo):

  • si on sait que foo est en fin de vie, et
  • l'utilisation implicite de std::move n'aura aucun effet sur la sémantique du code C++, à l'exception des effets sémantiques autorisés par la spécification C++.

Cela dépend des capacités d'optimisation du compilateur C++ s'il est capable de calculer quelles transformations de f(foo); foo.~Foo(); à f(std::move(foo)); foo.~Foo(); sont rentables en termes de performances ou de consommation de mémoire, tout en y adhérant. les règles de spécification C++.


Conceptuellement pour parler, les compilateurs C++ de l'année 2017, tels que GCC 6.3.0, sont capables d'optimiser ce code:

Foo meh() {
    Foo foo(args);
    foo.method(xyz);
    bar();
    return foo;
}

dans ce code:

void meh(Foo *retval) {
   new (retval) Foo(arg);
   retval->method(xyz);
   bar();
}

ce qui évite d'appeler le constructeur de copie et le destructeur de Foo.


Les compilateurs C++ de l'année 2017, tels que GCC 6.3.0, sont impossible à optimiser ces codes:

Foo meh_value() {
    Foo foo(args);
    Foo retval(foo);
    return retval;
}

Foo meh_pointer() {
    Foo *foo = get_foo();
    Foo retval(*foo);
    delete foo;
    return retval;
}

dans ces codes:

Foo meh_value() {
    Foo foo(args);
    Foo retval(std::move(foo));
    return retval;
}

Foo meh_pointer() {
    Foo *foo = get_foo();
    Foo retval(std::move(*foo));
    delete foo;
    return retval;
}

ce qui signifie qu'un programmeur de l'année 2017 doit spécifier explicitement ces optimisations.

1
atomsymbol