J'ai récemment lu sur les constructeurs de déplacement en C ++ (voir par exemple ici ) et j'essaie de comprendre comment ils fonctionnent et quand je dois les utiliser.
Pour autant que je sache, un constructeur de déplacement est utilisé pour atténuer les problèmes de performances causés par la copie de gros objets. La page wikipedia dit: "Un problème de performance chronique avec C ++ 03 est les copies profondes coûteuses et inutiles qui peuvent se produire implicitement lorsque des objets sont passés par valeur."
J'aborde normalement de telles situations
- en passant les objets par référence, ou
- en utilisant des pointeurs intelligents (par exemple boost :: shared_ptr) pour passer autour de l'objet (les pointeurs intelligents sont copiés à la place de l'objet).
Quelles sont les situations dans lesquelles les deux techniques ci-dessus ne sont pas suffisantes et l'utilisation d'un constructeur de mouvement est plus pratique?
c++
programming-practices
Giorgio
la source
la source
shared_ptr
juste pour des raisons de copie rapide) et si la sémantique de mouvement peut atteindre la même chose sans presque aucune pénalité de codage, sémantique et de propreté.Réponses:
La sémantique de mouvement introduit une dimension entière dans C ++ - elle n'est pas seulement là pour vous permettre de renvoyer des valeurs à moindre coût.
Par exemple, sans move-semantics
std::unique_ptr
ne fonctionne pas - regardezstd::auto_ptr
, qui a été déconseillé avec l'introduction de move-semantics et supprimé en C ++ 17. Déplacer une ressource est très différent de la copier. Il permet le transfert de propriété d'un article unique.Par exemple, ne regardons pas
std::unique_ptr
, car il est assez bien discuté. Regardons, disons, un objet tampon de sommet dans OpenGL. Un tampon de vertex représente la mémoire sur le GPU - il doit être alloué et désalloué à l'aide de fonctions spéciales, pouvant avoir des contraintes strictes sur la durée de vie. Il est également important qu'un seul propriétaire l'utilise.Maintenant, cela pourrait être fait avec un
std::shared_ptr
- mais cette ressource ne doit pas être partagée. Cela rend difficile l'utilisation d'un pointeur partagé. Vous pouvez utiliserstd::unique_ptr
, mais cela nécessite toujours une sémantique de mouvement.Évidemment, je n'ai pas implémenté de constructeur de déplacement, mais vous avez compris.
L'important ici est que certaines ressources ne sont pas copiables . Vous pouvez passer des pointeurs au lieu de les déplacer, mais à moins d'utiliser unique_ptr, il y a le problème de la propriété. Il vaut la peine d'être aussi clair que possible quant à l'intention du code, donc un constructeur de mouvement est probablement la meilleure approche.
la source
La sémantique de déplacement n'est pas nécessairement une grande amélioration lorsque vous retournez une valeur - et lorsque / si vous utilisez un
shared_ptr
(ou quelque chose de similaire), vous êtes probablement prématurément prématuré. En réalité, presque tous les compilateurs raisonnablement modernes font ce qu'on appelle l'optimisation de la valeur de retour (RVO) et l'optimisation de la valeur de retour nommée (NRVO). Cela signifie que lorsque vous retournez une valeur, au lieu de copier réellement la valeur du tout, ils transmettent simplement un pointeur / référence caché à l'endroit où la valeur va être affectée après le retour, et la fonction l'utilise pour créer la valeur là où elle va finir. Le standard C ++ inclut des dispositions spéciales pour permettre cela, donc même si (par exemple) votre constructeur de copie a des effets secondaires visibles, il n'est pas nécessaire d'utiliser le constructeur de copie pour renvoyer la valeur. Par exemple:L'idée de base ici est assez simple: créer une classe avec suffisamment de contenu, nous préférons éviter de la copier, si possible (le
std::vector
nous remplissons avec 32767 entiers aléatoires). Nous avons un ctor de copie explicite qui nous montrera quand / s'il est copié. Nous avons également un peu plus de code pour faire quelque chose avec les valeurs aléatoires dans l'objet, donc l'optimiseur n'éliminera pas (au moins facilement) tout ce qui concerne la classe juste parce qu'il ne fait rien.Nous avons ensuite du code pour renvoyer l'un de ces objets à partir d'une fonction, puis utilisons la sommation pour nous assurer que l'objet est vraiment créé, et pas simplement ignoré complètement. Lorsque nous l'exécutons, au moins avec les compilateurs les plus récents / modernes, nous constatons que le constructeur de copie que nous avons écrit ne fonctionne jamais du tout - et oui, je suis presque sûr que même une copie rapide avec un
shared_ptr
est encore plus lente que de ne pas copier du tout.Le déménagement vous permet de faire un bon nombre de choses que vous ne pourriez tout simplement pas faire (directement) sans elles. Considérez la partie «fusion» d'un tri de fusion externe - vous avez, disons, 8 fichiers que vous allez fusionner ensemble. Idéalement, vous aimeriez mettre les 8 de ces fichiers dans un
vector
- mais puisquevector
(à partir de C ++ 03) doit pouvoir copier des éléments, et queifstream
s ne peut pas être copié, vous êtes coincé avec certainsunique_ptr
/shared_ptr
, ou quelque chose sur cet ordre pour pouvoir les mettre dans un vecteur. Notez que même si (par exemple) nousreserve
espaceons dans levector
afin que nous soyons sûrs que nosifstream
s ne seront jamais vraiment copiés, le compilateur ne le saura pas, donc le code ne compilera pas même si nous savons que le constructeur de copie ne sera jamais utilisé de toute façon.Même s'il ne peut toujours pas être copié, en C ++ 11 un
ifstream
peut être déplacé. Dans ce cas, les objets ne seront probablement jamais déplacés, mais le fait qu'ils le soient si nécessaire garde le compilateur heureux, afin que nous puissions placer nosifstream
objetsvector
directement, sans aucun piratage de pointeur intelligent.Un vecteur qui fait développer est un exemple assez décent d'un temps que la sémantique de mouvement peut vraiment être / sont si utiles. Dans ce cas, RVO / NRVO n'aidera pas, car nous ne traitons pas la valeur de retour d'une fonction (ou quelque chose de très similaire). Nous avons un vecteur contenant certains objets et nous voulons déplacer ces objets dans un nouveau bloc de mémoire plus grand.
En C ++ 03, cela a été fait en créant des copies des objets dans la nouvelle mémoire, puis en détruisant les anciens objets dans l'ancienne mémoire. Faire toutes ces copies juste pour jeter les anciennes était cependant une perte de temps. En C ++ 11, vous pouvez vous attendre à ce qu'ils soient déplacés à la place. Cela nous permet généralement, essentiellement, de faire une copie superficielle au lieu d'une copie profonde (généralement beaucoup plus lente). En d'autres termes, avec une chaîne ou un vecteur (pour seulement quelques exemples), nous copions simplement le ou les pointeurs dans les objets, au lieu de faire des copies de toutes les données auxquelles ces pointeurs se réfèrent.
la source
Considérer:
Lors de l'ajout de chaînes à v, il se développera selon les besoins et à chaque réallocation, les chaînes devront être copiées. Avec les constructeurs de mouvements, ce n'est fondamentalement pas un problème.
Bien sûr, vous pouvez également faire quelque chose comme:
Mais cela ne fonctionnera bien que parce que les
std::unique_ptr
outils déplacent le constructeur.L'utilisation
std::shared_ptr
n'a de sens que dans des situations (rares) où vous avez réellement la propriété partagée.la source
string
nous avions une instanceFoo
où il a 30 membres de données? Launique_ptr
version ne serait pas plus efficace?Les valeurs de retour sont l'endroit où j'aimerais le plus souvent passer par valeur au lieu d'une sorte de référence. Pouvoir retourner rapidement un objet «sur la pile» sans pénalité de performance massive serait bien. D'un autre côté, ce n'est pas particulièrement difficile de contourner cela (les pointeurs partagés sont tellement faciles à utiliser ...), donc je ne suis pas sûr que cela vaille vraiment la peine de faire un travail supplémentaire sur mes objets juste pour pouvoir le faire.
la source