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();
c++
c++11
move-semantics
rvalue-reference
c++-faq
Tarentule
la source
la source
std::move()
créé une "copie" persistante.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'évaluationstd::move(expression)
.Réponses:
Premier exemple
Le premier exemple renvoie un temporaire qui est capturé
rval_ref
. Ce temporaire aura sa durée de vie prolongée au-delà de larval_ref
définition et vous pouvez l'utiliser comme si vous l'aviez pris par valeur. Ceci est très similaire à ce qui suit:sauf que dans ma réécriture, vous ne pouvez évidemment pas utiliser
rval_ref
de manière non constante.Deuxième exemple
Dans le deuxième exemple, vous avez créé une erreur d'exécution.
rval_ref
contient désormais une référence à la destructiontmp
à l'intérieur de la fonction. Avec un peu de chance, ce code planterait immédiatement.Troisième exemple
Votre troisième exemple est à peu près équivalent au premier. Le
std::move
surtmp
est 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
C'est à dire comme vous le feriez en C ++ 03.
tmp
est 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.la source
return my_local;
. Les déclarations de retour multiples sont correctes et n'inhiberont pas RVO.move
ne 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 lemove
de 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.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.
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.
la source
tmp
n'est pas déplacé dansrval_ref
, mais écrit directement dans à l'rval_ref
aide de RVO (c'est-à-dire élision de copie). Il y a une distinction entrestd::move
et copier l'élision. Unstd::move
peut 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.rval_ref
variables sont construites à l'aide du constructeur de déplacement destd::vector
. Aucun constructeur de copie n'est impliqué avec / sansstd::move
.tmp
est traité comme une instruction rvalue inreturn
dans ce cas.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:
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:
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é:
ou
ou
mais cela ne compilera pas!
la source
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 avecstd::vector<int>&& rval_ref
). Vous devrez toujoursstd::move()
les utiliser dansfoo(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 tellerval_ref
fonction, vous obtiendrez la référence standard à fiasco temporaire détruit.La plupart du temps, j'irais avec le modèle suivant:
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é.
É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,
if
conditions 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).la source
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.
la source
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 sansstd::move()
: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 C
représente des objets adaptateurs de courte durée dans le seul but de créer des instances destruct A
.Remarquez comment
struct A
se fait toujours copié surclass B
, même lorsque l'class B
objet est une valeur de R. C'est parce que le compilateur n'a aucun moyen de dire queclass B
l'instance destruct A
ne sera plus utilisée. Dansclass C
, le compilateur possède ces informationsstd::move()
, c'est pourquoi ilstruct A
est déplacé , sauf si l'instance declass C
est constante.la source