web-dev-qa-db-fra.com

C ++ 11 rvalues ​​et confusion sémantique de déplacement (instruction de retour)

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 aucune 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();
408
Tarantula

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é par rval_ref. La durée de vie de ce temporaire est au-delà de la définition de rval_ref et vous pouvez l'utiliser comme si vous l'aviez capturé 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_ref d'une 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_ref contient maintenant une référence au tmp détruit à 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::move sur tmp est inutile et peut en fait être une pessimisation des performances car il inhibera l'optimisation de la valeur de retour.

Le meilleur moyen 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. tmp est implicitement traité comme une valeur rvalue dans l'instruction return. Il sera soit retourné via return-value-optimization (pas de copie, pas de déplacement), ou si le compilateur décide qu'il ne peut pas exécuter RVO, alors il tilisera le constructeur de déplacement de vecteur pour effectuer le retour . Ce n'est que si RVO n'est pas exécuté et si le type renvoyé n'a pas de constructeur de déplacement, le constructeur de copie sera utilisé pour le retour.

536
Howard Hinnant

Aucun d'entre 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 standard. Vous écrivez exactement comment 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é. Le tilisateur d'une classe ne traite pas ses références rvalue dans la grande majorité des cas.

41
Puppy

La réponse simple est que vous devriez écrire du code pour les références rvalue comme vous le feriez avec du code de références habituel, et vous devriez les traiter mentalement la même chose 99% du temps. Cela inclut toutes les anciennes règles sur le renvoi de références (c'est-à-dire ne jamais renvoyer de référence à une variable locale).

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

L'un des gros avantages du constructeur et de l'affectation de déplacement est que, si vous les définissez, le compilateur peut les utiliser dans les cas où RVO (optimisation de la valeur renvoyée) et NRVO (optimisation nommée de la valeur renvoyée) ne sont pas invoqués. C'est assez énorme pour renvoyer des objets coûteux tels que des conteneurs et des chaînes de valeur efficacement à 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 de fonctions normales. Cela vous permet d'écrire des conteneurs avec des surcharges pour les références const (const foo & other) et rvalue reference (foo && other). Même si l'argument est trop difficile à passer avec un simple appel de constructeur, cela 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 permettre des surcharges de déplacement pour presque tout (clé de hachage et valeurs, insertion de vecteur, etc.). C'est là que vous les verrez le plus souvent.

Vous pouvez également les utiliser pour des fonctions normales. 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'un très bon usage, mais dans ma bibliothèque de rendu, j'ai attribué une chaîne à toutes les ressources chargées, de sorte qu'il est 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 cela me permet de tirer parti du fait que je devais déjà créer la chaîne la plupart du temps et éviter de la copier à nouveau. Ce n'est pas un code très performant, mais un bon exemple des possibilités offertes aux utilisateurs. Ce code nécessite en fait que la variable soit temporaire pour l'appel ou que std :: move est 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);
16
Zoner

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

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 les erreurs (inexpérimentées) du programmeur 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();

De toute évidence, il existe des cas (bien que plutôt rares) dans lesquels une fonction renvoie réellement un T&&, référence à un objet non temporaire que vous pouvez déplacer dans votre objet.

En ce qui concerne RVO: ces mécanismes fonctionnent généralement et le compilateur peut bien éviter la copie, mais dans les cas où le chemin de retour n’est pas évident (exceptions, if conditions déterminant l’objet nommé que vous retournerez, et probablement quelques autres) rrefs est votre sauveur (même si potentiellement plus cher).

3
Red XIII

Aucun de ceux-ci ne fera de copie supplémentaire. Même si RVO n'est pas utilisé, la nouvelle norme indique que la construction de déménagement est préférable de copier lorsque vous effectuez des retours, je crois.

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

2
Edward Strange

Comme déjà mentionné dans les commentaires sur la première réponse, la construction return std::move(...); peut faire la différence dans les cas autres que le renvoi 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, return std::move(some_member); n’a de sens que si vous souhaitez réellement déplacer le membre de la classe, par exemple. dans le cas où class C représente des objets adaptateur de courte durée dans le seul but de créer des instances de struct A.

Remarquez comment struct A obtient toujours copié sur class B, même lorsque l'objet class B est une valeur R. En effet, le compilateur n'a aucun moyen de savoir que l'instance de class B de struct A ne sera plus utilisée. Dans class C, le compilateur a cette information de std::move(), raison pour laquelle struct A obtient déplacé, sauf si l'instance de class C est constante.

0
Andrej Podzimek