web-dev-qa-db-fra.com

Qu'est-ce qui rend les objets en mouvement plus rapides que la copie?

J'ai entendu Scott Meyers dire "std::move() ne bouge rien" ... mais je n'ai pas compris ce que cela signifie.

Donc, pour préciser ma question, considérez ce qui suit:

class Box { /* things... */ };

Box box1 = some_value;
Box box2 = box1;    // value of box1 is copied to box2 ... ok

Qu'en est-il de:

Box box3 = std::move(box1);

Je comprends les règles de lvalue et rvalue mais ce que je ne comprends pas, c'est ce qui se passe réellement dans la mémoire? Est-ce simplement copier la valeur d'une manière différente, partager une adresse ou quoi? Plus précisément: qu'est-ce qui rend le déplacement plus rapide que la copie?

Je pense simplement que comprendre cela rendrait tout clair pour moi. Merci d'avance!

EDIT: Veuillez noter que je ne pose pas de question sur l'implémentation de std::move() ou sur des éléments syntaxiques.

33
Laith

Comme @ gudokrépondu avant , tout est dans l'implémentation ... Ensuite, un peu est dans le code utilisateur.

La mise en oeuvre

Supposons que nous parlons du constructeur de copie pour attribuer une valeur à la classe actuelle.

L'implémentation que vous fournirez prendra en compte deux cas:

  1. le paramètre est une valeur l, vous ne pouvez donc pas le toucher, par définition
  2. le paramètre est une valeur r, donc, implicitement, le temporaire ne vivra pas beaucoup plus longtemps que vous ne l'utilisez, donc, au lieu de copier son contenu, vous pourriez voler son contenu

Les deux sont implémentés à l'aide d'une surcharge:

Box::Box(const Box & other)
{
   // copy the contents of other
}

Box::Box(Box && other)
{
   // steal the contents of other
}

L'implémentation pour les classes légères

Disons que votre classe contient deux entiers: vous ne pouvez pas voler ceux-ci car ce sont des valeurs brutes simples. La seule chose qui semblerait comme voler serait de copier les valeurs , puis mettez l'original à zéro, ou quelque chose comme ça ... Ce qui n'a aucun sens pour les simples entiers. Pourquoi ce travail supplémentaire?

Donc, pour les classes de valeurs légères, proposer deux implémentations spécifiques, une pour la valeur l et une pour les valeurs r, n'a aucun sens.

Offrir uniquement la mise en œuvre de la valeur l sera plus que suffisant.

L'implémentation pour les classes plus lourdes

Mais dans le cas de certaines classes lourdes (c'est-à-dire std :: string, std :: map, etc.), la copie implique potentiellement un coût, généralement dans les allocations. Donc, idéalement, vous voulez l'éviter autant que possible. C'est là que voler les données des temporaires deviennent intéressants.

Supposons que votre Box contient un pointeur brut vers un HeavyResource dont la copie est coûteuse. Le code devient:

Box::Box(const Box & other)
{
   this->p = new HeavyResource(*(other.p)) ; // costly copying
}

Box::Box(Box && other)
{
   this->p = other.p ; // trivial stealing, part 1
   other.p = nullptr ; // trivial stealing, part 2
}

Il est clair qu'un constructeur (le constructeur de copie, qui a besoin d'une allocation) est beaucoup plus lent qu'un autre (le constructeur de mouvement, qui n'a besoin que d'affectations de pointeurs bruts).

Quand est-il sécuritaire de "voler"?

Le truc c'est que: Par défaut, le compilateur invoquera le "code rapide" uniquement lorsque le paramètre est temporaire (c'est un peu plus subtil, mais soyez indulgent avec moi ...).

Pourquoi?

Parce que le compilateur peut garantir que vous pouvez voler un objet sans aucun problème uniquement si cet objet est temporaire (ou sera détruit peu de temps après). Pour les autres objets, le vol signifie que vous avez soudainement un objet qui est valide, mais dans un état non spécifié, qui pourrait encore être utilisé plus bas dans le code. Menant éventuellement à des plantages ou à des bugs:

Box box3 = static_cast<Box &&>(box1); // calls the "stealing" constructor
box1.doSomething();         // Oops! You are using an "empty" object!

Mais parfois, vous voulez la performance. Alors, comment tu fais?

Le code utilisateur

Comme vous l'avez écrit:

Box box1 = some_value;
Box box2 = box1;            // value of box1 is copied to box2 ... ok
Box box3 = std::move(box1); // ???

Ce qui se passe pour box2 est que, comme box1 est une valeur l, le premier constructeur de copie "lent" est invoqué. Il s'agit du code C++ 98 normal.

Maintenant, pour box3, quelque chose de drôle se produit: std :: move retourne le même box1, mais comme référence de valeur r, au lieu d'une valeur l. Donc la ligne:

Box box3 = ...

... N'invoquera PAS le constructeur de copie sur box1.

Il invoquera INSTEAD le constructeur volant (officiellement connu sous le nom de constructeur de déplacement) sur box1.

Et comme votre implémentation du constructeur de déplacement pour Box "vole" le contenu de box1, à la fin de l'expression, box1 est dans un état valide mais non spécifié (généralement, il sera vide), et box3 contient le (précédent) contenu de la boîte 1.

Qu'en est-il de l'état valide mais non spécifié d'une classe déplacée?

Bien sûr, écrire std :: move sur une valeur l signifie que vous promettez de ne plus utiliser cette valeur l. Ou vous le ferez, très, très soigneusement.

Citant le projet standard C++ 17 (C++ 11 était: 17.6.5.15):

20.5.5.15 État déplacé des types de bibliothèques [lib.types.movedfrom]

Les objets de types définis dans la bibliothèque standard C++ peuvent être déplacés de (15.8). Les opérations de déplacement peuvent être spécifiées explicitement ou générées implicitement. Sauf indication contraire, ces objets déplacés doivent être placés dans un état valide mais non spécifié.

Il s'agissait des types de la bibliothèque standard, mais c'est quelque chose que vous devez suivre pour votre propre code.

Cela signifie que la valeur déplacée peut maintenant contenir n'importe quelle valeur, qu'elle soit vide, nulle ou une valeur aléatoire. Par exemple. pour tout ce que vous savez, votre chaîne "Bonjour" deviendrait une chaîne vide "", ou deviendrait "Enfer", ou même "Au revoir", si le réalisateur estime que c'est la bonne solution. Cependant, il doit toujours s'agir d'une chaîne valide, avec tous ses invariants respectés.

Donc, à la fin, à moins que l'implémenteur (d'un type) ne s'engage explicitement à un comportement spécifique après un déplacement, vous devez agir comme si vous ne saviez rien sur une valeur déplacée (de ce type).

Conclusion

Comme dit ci-dessus, le std :: move fait rien. Il indique seulement au compilateur: "Vous voyez cette valeur l? Veuillez la considérer comme une valeur r, juste une seconde".

Donc, dans:

Box box3 = std::move(box1); // ???

... le code utilisateur (c'est-à-dire std :: move) indique au compilateur que le paramètre peut être considéré comme une valeur r pour cette expression, et donc, le constructeur de déplacement sera appelé.

Pour l'auteur du code (et le réviseur de code), le code indique en fait qu'il est correct de voler le contenu de box1, de le déplacer dans box3. L'auteur du code devra alors s'assurer que box1 n'est plus utilisé (ou utilisé très très soigneusement). C'est leur responsabilité.

Mais au final, c'est l'implémentation du constructeur de déplacement qui fera la différence, principalement en termes de performances: si le constructeur de déplacement vole réellement le contenu de la valeur r, alors vous verrez une différence. S'il fait autre chose, l'auteur a menti à ce sujet, mais c'est un autre problème ...

49
paercebal

Tout est question de mise en œuvre. Considérez une classe de chaîne simple:

class my_string {
  char* ptr;
  size_t capacity;
  size_t length;
};

La sémantique de copie nous oblige à faire une copie complète de la chaîne, y compris l'allocation d'un autre tableau dans la mémoire dynamique et la copie *ptr contenu là-bas, ce qui est cher.

La sémantique de move nous oblige seulement à transférer la valeur du pointeur lui-même à un nouvel objet sans dupliquer le contenu de la chaîne.

Si, bien sûr, la classe n'utilise pas de mémoire dynamique ou de ressources système, il n'y a pas de différence entre le déplacement et la copie en termes de performances.

15
gudok

La fonction std::move() doit être comprise comme un transtypage au type rvalue correspondant, qui permet de déplacer le objet au lieu de copier.


Cela pourrait ne faire aucune différence:

std::cout << std::move(std::string("Hello, world!")) << std::endl;

Ici, la chaîne était déjà une valeur r, donc std::move() n'a rien changé.


Il peut permettre le déplacement, mais peut toujours entraîner une copie:

auto a = 42;
auto b = std::move(a);

Il n'y a pas de moyen plus efficace de créer un entier qui le copie simplement.


Là où va provoquer un mouvement, c'est quand l'argument

  1. est une référence lvalue ou lvalue,
  2. a un constructeur de déplacement ou opérateur d'affectation de déplacement , et
  3. est (implicitement ou explicitement) la source d'une construction ou d'une affectation.

Même dans ce cas, ce n'est pas la move() elle-même qui en fait déplace les données, c'est la construction ou l'affectation. std:move() est simplement le transtypage qui permet que cela se produise, même si vous avez une valeur l pour commencer. Et le déplacement peut se produire sans std::move Si vous commencez avec une valeur r. Je pense que c'est le sens de la déclaration de Meyers.

1
Toby Speight