C ++ 11 rvalues ​​and move semantics confusion (instruction de retour)

435

J'essaie de comprendre les références rvalue et de déplacer la sémantique de C ++ 11.

Quelle est la différence entre ces exemples et lequel d'entre eux ne fera pas de copie vectorielle?

Premier exemple

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

Deuxième exemple

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Troisième exemple

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();
Tarentule
la source
51
Veuillez ne jamais renvoyer de variables locales par référence. Une référence rvalue est toujours une référence.
fredoverflow
63
C'était évidemment intentionnel afin de comprendre les différences sémantiques entre les exemples lol
Tarantula
@FredOverflow Ancienne question, mais il m'a fallu une seconde pour comprendre votre commentaire. Je pense que la question avec # 2 était de savoir si std::move()créé une "copie" persistante.
3Dave
5
@DavidLively std::move(expression)ne crée rien, il transforme simplement l'expression en une valeur x. Aucun objet n'est copié ou déplacé au cours de l'évaluation std::move(expression).
fredoverflow

Réponses:

563

Premier exemple

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

Le premier exemple renvoie un temporaire qui est capturé rval_ref. Ce temporaire aura sa durée de vie prolongée au-delà de la rval_refdéfinition et vous pouvez l'utiliser comme si vous l'aviez pris par valeur. Ceci est très similaire à ce qui suit:

const std::vector<int>& rval_ref = return_vector();

sauf que dans ma réécriture, vous ne pouvez évidemment pas utiliser rval_refde manière non constante.

Deuxième exemple

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Dans le deuxième exemple, vous avez créé une erreur d'exécution. rval_refcontient désormais une référence à la destruction tmpà l'intérieur de la fonction. Avec un peu de chance, ce code planterait immédiatement.

Troisième exemple

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Votre troisième exemple est à peu près équivalent au premier. Le std::movesur tmpest inutile et peut effectivement être un pessimization de performance car il inhibe l' optimisation de la valeur de retour.

La meilleure façon de coder ce que vous faites est:

Meilleur entrainement

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

C'est à dire comme vous le feriez en C ++ 03. tmpest implicitement traité comme une valeur r dans l'instruction return. Il sera soit retourné via l'optimisation de la valeur de retour (pas de copie, pas de déplacement), ou si le compilateur décide qu'il ne peut pas exécuter RVO, alors il utilisera le constructeur de déplacement de vector pour effectuer le retour . Ce n'est que si RVO n'est pas effectué et si le type retourné n'a pas de constructeur de déplacement que le constructeur de copie sera utilisé pour le retour.

Howard Hinnant
la source
65
Les compilateurs effectueront un RVO lorsque vous retournerez un objet local par valeur, et le type du local et le retour de la fonction sont les mêmes, et ni l'un ni l'autre n'est qualifié par cv (ne retournez pas de types const). Évitez de revenir avec la condition (:?) Car elle peut inhiber le RVO. N'encapsulez pas le local dans une autre fonction qui renvoie une référence au local. Justement return my_local;. Les déclarations de retour multiples sont correctes et n'inhiberont pas RVO.
Howard Hinnant
27
Il y a une mise en garde: lors du retour d'un membre d'un objet local, le mouvement doit être explicite.
boycy
5
@NoSenseEtAl: Il n'y a pas de création temporaire sur la ligne de retour. movene crée pas de temporaire. Il convertit une lvalue en une xvalue, ne faisant aucune copie, ne créant rien, ne détruisant rien. Cet exemple est exactement la même situation que si vous retourniez par lvalue-reference et supprimiez le movede la ligne de retour: Dans les deux cas, vous avez une référence pendant à une variable locale à l'intérieur de la fonction et qui a été détruite.
Howard Hinnant
15
"Plusieurs déclarations de retour sont correctes et n'inhiberont pas RVO": uniquement si elles renvoient la même variable.
Deduplicator
5
@Deduplicator: Vous avez raison. Je ne parlais pas aussi précisément que je le souhaitais. Je voulais dire que plusieurs instructions de retour n'interdisent pas le compilateur de RVO (même si cela le rend impossible à implémenter), et donc l'expression de retour est toujours considérée comme une rvalue.
Howard Hinnant
42

Aucun d'eux ne copiera, mais le second fera référence à un vecteur détruit. Les références rvalue nommées n'existent presque jamais dans le code normal. Vous l'écrivez comme vous auriez écrit une copie en C ++ 03.

std::vector<int> return_vector()
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

Sauf que maintenant, le vecteur est déplacé. L' utilisateur d'une classe ne gère pas ses références de valeur dans la grande majorité des cas.

Chiot
la source
Êtes-vous vraiment sûr que le troisième exemple va faire une copie vectorielle?
Tarantula
@Tarantula: Cela va détruire votre vecteur. Qu'il l'ait copié ou non avant de le casser n'a pas vraiment d'importance.
Puppy
4
Je ne vois aucune raison pour la cassure que vous proposez. Il est tout à fait correct de lier une variable de référence rvalue locale à une rvalue. Dans ce cas, la durée de vie de l'objet temporaire est étendue à la durée de vie de la variable de référence rvalue.
fredoverflow
1
Juste une précision, puisque j'apprends cela. Dans ce nouvel exemple, le vecteur tmpn'est pas déplacé dans rval_ref, mais écrit directement dans à l' rval_refaide de RVO (c'est-à-dire élision de copie). Il y a une distinction entre std::moveet copier l'élision. Un std::movepeut encore impliquer certaines données à copier; dans le cas d'un vecteur, un nouveau vecteur est en fait construit dans le constructeur de copie et les données sont allouées, mais la majeure partie du tableau de données n'est copiée qu'en copiant le pointeur (essentiellement). L'élision de copie évite 100% de toutes les copies.
Mark Lakata
@MarkLakata C'est NRVO, pas RVO. NRVO est facultatif, même en C ++ 17. S'il n'est pas appliqué, la valeur de retour et les rval_refvariables sont construites à l'aide du constructeur de déplacement de std::vector. Aucun constructeur de copie n'est impliqué avec / sans std::move. tmpest traité comme une instruction rvalue in returndans ce cas.
Daniel Langr
16

La réponse simple est que vous devez écrire du code pour les références rvalue comme vous le feriez pour le code des références régulières, et vous devez les traiter de la même manière mentalement 99% du temps. Cela inclut toutes les anciennes règles concernant le renvoi de références (c.-à-d. Ne renvoyez jamais une référence à une variable locale).

À moins que vous n'écriviez une classe de conteneur de modèle qui doit tirer parti de std :: forward et être capable d'écrire une fonction générique qui accepte des références lvalue ou rvalue, cela est plus ou moins vrai.

L'un des grands avantages du constructeur de déplacement et de l'affectation de déplacement est que si vous les définissez, le compilateur peut les utiliser dans les cas où le RVO (optimisation de la valeur de retour) et NRVO (l'optimisation de la valeur de retour nommée) n'ont pas été invoqués. C'est assez énorme pour renvoyer efficacement des objets coûteux comme des conteneurs et des chaînes de valeur à partir de méthodes.

Maintenant, là où les choses deviennent intéressantes avec les références rvalue, c'est que vous pouvez également les utiliser comme arguments pour les fonctions normales. Cela vous permet d'écrire des conteneurs qui ont des surcharges pour la référence const (const foo & other) et la référence rvalue (foo && other). Même si l'argument est trop compliqué à passer avec un simple appel de constructeur, il peut toujours être fait:

std::vector vec;
for(int x=0; x<10; ++x)
{
    // automatically uses rvalue reference constructor if available
    // because MyCheapType is an unamed temporary variable
    vec.push_back(MyCheapType(0.f));
}


std::vector vec;
for(int x=0; x<10; ++x)
{
    MyExpensiveType temp(1.0, 3.0);
    temp.initSomeOtherFields(malloc(5000));

    // old way, passed via const reference, expensive copy
    vec.push_back(temp);

    // new way, passed via rvalue reference, cheap move
    // just don't use temp again,  not difficult in a loop like this though . . .
    vec.push_back(std::move(temp));
}

Les conteneurs STL ont été mis à jour pour avoir des surcharges de mouvement pour presque tout (clé et valeurs de hachage, insertion de vecteur, etc.), et c'est là que vous les verrez le plus.

Vous pouvez également les utiliser pour des fonctions normales et si vous ne fournissez qu'un argument de référence rvalue, vous pouvez forcer l'appelant à créer l'objet et laisser la fonction effectuer le déplacement. C'est plus un exemple qu'une très bonne utilisation, mais dans ma bibliothèque de rendu, j'ai assigné une chaîne à toutes les ressources chargées, afin qu'il soit plus facile de voir ce que chaque objet représente dans le débogueur. L'interface ressemble à ceci:

TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
{
    std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
    tex->friendlyName = std::move(friendlyName);
    return tex;
}

C'est une forme d '«abstraction qui fuit» mais me permet de profiter du fait que je devais déjà créer la chaîne la plupart du temps, et éviter d'en faire une autre copie. Ce n'est pas exactement du code haute performance, mais c'est un bon exemple des possibilités que les gens maîtrisent cette fonctionnalité. Ce code nécessite en fait que la variable soit temporaire à l'appel, ou std :: move invoqué:

// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));

ou

// explicit move (not going to use the variable 'str' after the create call)
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));

ou

// explicitly make a copy and pass the temporary of the copy down
// since we need to use str again for some reason
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));

mais cela ne compilera pas!

string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);
Zoner
la source
3

Pas une réponse en soi , mais une ligne directrice. La plupart du temps, il est inutile de déclarer une T&&variable locale (comme vous l'avez fait avec std::vector<int>&& rval_ref). Vous devrez toujours std::move()les utiliser dans foo(T&&)les méthodes de type. Il y a aussi le problème qui a déjà été mentionné que lorsque vous essayez de renvoyer une telle rval_reffonction, vous obtiendrez la référence standard à fiasco temporaire détruit.

La plupart du temps, j'irais avec le modèle suivant:

// Declarations
A a(B&&, C&&);
B b();
C c();

auto ret = a(b(), c());

Vous ne détenez aucune référence aux objets temporaires retournés, vous évitez ainsi l'erreur de programmeur (inexpérimenté) qui souhaite utiliser un objet déplacé.

auto bRet = b();
auto cRet = c();
auto aRet = a(std::move(b), std::move(c));

// Either these just fail (assert/exception), or you won't get 
// your expected results due to their clean state.
bRet.foo();
cRet.bar();

Évidemment, il existe des cas (bien que plutôt rares) où une fonction renvoie vraiment un T&&qui est une référence à un objet non temporaire que vous pouvez déplacer dans votre objet.

Concernant RVO: ces mécanismes fonctionnent généralement et le compilateur peut très bien éviter la copie, mais dans les cas où le chemin de retour n'est pas évident (exceptions, ifconditions déterminant l'objet nommé que vous retournerez, et probablement quelques autres) les références sont vos sauveurs (même si potentiellement plus coûteux).

Rouge XIII
la source
2

Aucun de ceux-ci ne fera de copie supplémentaire. Même si RVO n'est pas utilisé, la nouvelle norme dit que la construction de mouvement est préférable de copier lors des retours, je crois.

Je crois que votre deuxième exemple provoque un comportement indéfini, car vous renvoyez une référence à une variable locale.

Edward Strange
la source
1

Comme déjà mentionné dans les commentaires de la première réponse, la return std::move(...);construction peut faire la différence dans des cas autres que le retour de variables locales. Voici un exemple exécutable qui documente ce qui se passe lorsque vous retournez un objet membre avec et sans std::move():

#include <iostream>
#include <utility>

struct A {
  A() = default;
  A(const A&) { std::cout << "A copied\n"; }
  A(A&&) { std::cout << "A moved\n"; }
};

class B {
  A a;
 public:
  operator A() const & { std::cout << "B C-value: "; return a; }
  operator A() & { std::cout << "B L-value: "; return a; }
  operator A() && { std::cout << "B R-value: "; return a; }
};

class C {
  A a;
 public:
  operator A() const & { std::cout << "C C-value: "; return std::move(a); }
  operator A() & { std::cout << "C L-value: "; return std::move(a); }
  operator A() && { std::cout << "C R-value: "; return std::move(a); }
};

int main() {
  // Non-constant L-values
  B b;
  C c;
  A{b};    // B L-value: A copied
  A{c};    // C L-value: A moved

  // R-values
  A{B{}};  // B R-value: A copied
  A{C{}};  // C R-value: A moved

  // Constant L-values
  const B bc;
  const C cc;
  A{bc};   // B C-value: A copied
  A{cc};   // C C-value: A copied

  return 0;
}

Vraisemblablement, cela return std::move(some_member);n'a de sens que si vous voulez réellement déplacer le membre de classe particulier, par exemple dans un cas où class Creprésente des objets adaptateurs de courte durée dans le seul but de créer des instances de struct A.

Remarquez comment struct Ase fait toujours copié sur class B, même lorsque l' class Bobjet est une valeur de R. C'est parce que le compilateur n'a aucun moyen de dire que class Bl'instance de struct Ane sera plus utilisée. Dans class C, le compilateur possède ces informations std::move(), c'est pourquoi il struct Aest déplacé , sauf si l'instance de class Cest constante.

Andrej Podzimek
la source