160 votes

Quand doit-on utiliser std::move sur la valeur de retour d'une fonction ?

Dans ce cas

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

Je suis presque sûr que le déplacement n'est pas nécessaire, parce que le nouveau créé Foo sera une valeur x.

Mais que faire dans des cas comme celui-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?
}

C'est là que le déménagement est nécessaire, je suppose ?

163voto

Steve Jessop Points 166970

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

Lorsque les critères d'exclusion d'une opération de copie sont remplis ou le seraient remplis, sauf que l'objet source est un paramètre de fonction, et que l'objet à copier est désigné par une lvalue, la résolution de surcharge pour sélectionner le constructeur de la copie est first exécutée comme si l'objet était désigné par un paramètre de fonction. comme si l'objet était désigné par une rvalue.

return foo; est un cas de NRVO, l'élision de la copie est donc autorisée. foo est une lvalue. Ainsi, le constructeur sélectionné pour la "copie" de foo à la valeur de retour de meh doit être le constructeur du mouvement s'il en existe un.

Ajout de move a cependant un effet potentiel : il empêche le coup d'être élidé, parce que return std::move(foo); es no éligibles pour le NRVO.

Pour autant que je sache, 12.8/32 expose les sólo conditions dans lesquelles une copie d'une lvalue peut être remplacée par un déplacement. Le compilateur n'est pas autorisé en général à détecter qu'une lvalue est inutilisée après la copie (en utilisant la DFA, par exemple), et à effectuer le changement 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, alors la règle "as-if" s'applique.

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

  • vous voulez qu'il soit déplacé, et
  • il s'agit d'une lvalue, et
  • il n'est pas éligible à l'élision de la copie, et
  • il ne s'agit pas du nom d'un paramètre de fonction par valeur.

Considérant que c'est assez compliqué et que les mouvements sont généralement bon marché, vous pourriez dire que dans le code non-modèle, vous pouvez simplifier un peu cela. Utilisez std::move quand :

  • vous voulez qu'il soit déplacé, et
  • il s'agit d'une lvalue, et
  • vous ne pouvez pas vous en préoccuper.

En suivant les règles simplifiées, vous sacrifiez un peu d'élision de mouvement. Pour des types comme std::vector qui sont peu coûteux à déplacer, vous ne le remarquerez probablement jamais (et si vous le remarquez, vous pouvez l'optimiser). Pour des types comme std::array qui sont coûteux à déplacer, ou pour les modèles pour lesquels vous n'avez aucune idée si les déplacements sont bon marché ou non, il est plus probable que vous vous inquiétiez de ce problème.

46voto

Andy Prowl Points 62121

Le déplacement est inutile dans les deux cas. Dans le second cas, std::move est superflu car vous renvoyez une variable locale par valeur, et le compilateur comprendra que puisque vous n'allez plus utiliser cette variable locale, elle peut être déplacée au lieu d'être copiée.

32voto

Sur une valeur de retour, si l'expression de retour se réfère directement au nom d'une lvalue locale (c'est-à-dire, à ce stade, une xvalue), il n'est pas nécessaire d'utiliser l'attribut std::move . En revanche, si l'expression de retour est no l'identifiant, il ne sera pas déplacé automatiquement, donc, par exemple, vous aurez 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, vous devez éviter l'utilisation explicite de la fonction std::move . Le compilateur doit (et le feront à l'avenir) se déplacent automatiquement dans ces cas, et l'ajout de std::move pourrait affecter d'autres optimisations.

30voto

Yakk Points 31636

Il y a beaucoup de réponses sur les circonstances dans lesquelles il ne faut pas le déplacer, mais la question est "quand faut-il le déplacer ?"

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 lorsque vous avez une fonction qui prend une référence rvalue, la modifie, puis en renvoie une copie. (En c++20 Le comportement change ici) 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 permet également de prendre des paramètres sans valeur réelle.

En gros, si vous avez une référence rvalue à l'intérieur d'une fonction que vous voulez retourner par déplacement, vous devez appeler std::move . Si vous avez une variable locale (qu'elle soit un paramètre ou non), la retourner implicitement move (et ce déplacement implicite peut être élidé, alors qu'un déplacement explicite ne le peut pas). Si vous avez une fonction ou une opération qui prend des variables locales, et retourne une référence à ladite variable locale, vous devez std::move pour que le mouvement se produise (à titre d'exemple, le trinaire ?: opérateur).

3voto

atomsymbol Points 286

L'utilisation d'un compilateur C++ est gratuite std::move(foo) :

  • si l'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++ autre que les effets sémantiques autorisés par la spécification C++.

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


Conceptuellement En fait, les compilateurs C++ de l'année 2017, tels que GCC 6.3.0, sont les suivants capable 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 incapable d'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 de telles optimisations.

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