C++11 rvalues et la sémantique de déplacement de la confusion (return)
J'essaie de comprendre les références rvalue et la sémantique de déplacement de C++11.
Quelle est la différence entre ces exemples, et qui est en passe de ne pas faire le vecteur de la copie?
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();
- Veuillez ne pas retourner les variables locales par référence, jamais. Référence rvalue est toujours une référence.
- C'est évidemment intentionnel dans le but de comprendre les différences sémantiques entre les exemples lol
- Vieille question, mais il m'a fallu une seconde pour comprendre votre commentaire. Je pense que la question #2 était de savoir si
std::move()
créé une persistance de la "copie". std::move(expression)
ne crée rien, il rejette simplement l'expression d'une value. Pas d'objets sont copiés ou déplacés dans le processus d'évaluation destd::move(expression)
.
Vous devez vous connecter pour publier un commentaire.
Premier exemple
Le premier exemple renvoie une temporaire qui est capturé par
rval_ref
. Que temporaire aura sa durée de vie prolongée au-delà de larval_ref
définition et vous pouvez l'utiliser comme si vous aviez pris par valeur. Ceci est très similaire à la suivante:sauf que dans mon réécriture de toute évidence vous ne pouvez pas utiliser
rval_ref
dans un non-const manière.Deuxième exemple
Dans le deuxième exemple, vous avez créé une erreur d'exécution.
rval_ref
détient désormais une référence pour l'été détruitstmp
l'intérieur de la fonction. Avec un peu de chance, ce code serait immédiatement crash.Troisième exemple
Votre troisième exemple est à peu près équivalent à votre premier. Le
std::move
surtmp
est inutile et peut en fait être une performance pessimization qu'il va inhiber la valeur de retour d'optimisation.Le meilleur moyen de code de ce que vous faites est:
Meilleures pratiques
I. e. tout comme vous le feriez en C++03.
tmp
est implicitement traitée comme une rvalue dans l'instruction de retour. Il vous sera retourné par retour de valeur-de l'optimisation (pas de copie, pas de déplacement), ou si le compilateur décide qu'il ne peut pas effectuer RVO, puis il sera l'utilisation du vecteur constructeur de déplacement pour faire le retour. Seulement si RVO n'est pas effectuée, et si le retour n'a pas un constructeur de déplacement serait le constructeur de copie être utilisée pour le retour.return my_local;
. Plusieurs instructions return sont ok et n'empêchera pas RVO.move
ne pas créer un temporaire. Il jette une lvalue à un xvalue, sans faire de copies, de ne rien créer, rien détruire. Cet exemple est exactement la même situation que si vous retourné par lvalue de référence et enlevé lemove
à partir de la ligne de retour: de toute façon vous avez un bancales référence à une variable locale à l'intérieur de la fonction et qui a été détruite.tmp
) dans les "Meilleures pratiques" de l'article, c'est la NRVO que des coups de pied dans, pas la RVO. Ces deux optimisations. Autre que cela, super réponse!const
pour RVO?return_vector()
est une rvalue, étant donné que la fonction de renvoi d'un objet par valeur. Lorsque cette expression est utilisée pour construire un objet sur le site d'appel, de résolution de surcharge choisir un constructeur de déplacement si elle existe. Si l'objet est déjà construit, puis la surcharge de résolution, au lieu de choisir un opérateur d'affectation. Puisque le membre de droite est une rvalue, il va choisir l'opérateur d'assignation de déplacement si elle existe.string+string
mais heureusement corrigé avant C++11 en cours de finalisation. Ici est un peu maladroit moyen de contourner cela: wandbox.org/permlink/HQPGEOMAUXwUCMb4return_vector(std::vector& x)
+vector::resize(0)
pourrait sauver quelques mallocs à travers de multiplesreturn_vector
appels, et d'être encore plus efficace que la NRVO, au prix d'une plus grande utilisation de la mémoire globale? Plus précis code: stackoverflow.com/questions/10476665/...Aucun d'entre eux ne copie, mais la seconde se réfère à une détruits vecteur. Nommé références rvalue presque jamais exister dans le code standard. Vous l'écrire juste la façon dont vous avez écrit une copie en C++03.
Sauf que maintenant, le vecteur est déplacé. Le utilisateur d'une classe n'a pas affaire avec ses références rvalue dans la grande majorité des cas.
tmp
n'est pas déplacé enrval_ref
, mais écrit directement dansrval_ref
à l'aide de RVO (c'est à dire copier élision). Il y a une distinction entrestd::move
et copie élision. Unstd::move
peut toujours comprendre certaines données à copier; dans le cas d'un vecteur, un nouveau vecteur est construit dans le constructeur de copie et de données est affectée, mais l'essentiel du tableau de données est seulement copié par la copie du pointeur (pour l'essentiel). La copie élision évite de 100% de toutes les copies.rval_ref
variables sont construites en utilisant le constructeur de déplacement destd::vector
. Il n'y a pas de constructeur de copie impliqué à la fois avec / sansstd::move
.tmp
est traité comme un rvalue dansreturn
instruction dans cette affaire.tmp
, puis NRVO pourrait s'appliquer (ou pas, puisque c'est facultatif). Sireturn_vector
était tout simplement{return std::vector{1,2,3,4,5};
il serait RVO. Mon point est que, avec un compilateur décent qui peut faire de la RVO et NRVO,rval_ref
n'est pas exemplaire construit ou déplacer construit - il est construit directement commestd::vector<int>{1,2,3,4,5}
.La réponse est simple, vous devez écrire du code pour les références rvalue comme vous le feriez régulièrement des références de code, et vous devez les traiter de la même mentalement 99% du temps. Cela comprend toutes les anciennes règles sur le retour de références (c'est à dire de ne jamais retourner une référence à une variable locale).
À moins que vous écrivez un modèle de classe conteneur, qui doit profiter de std::forward et être capable d'écrire une fonction générique qui prend soit lvalue ou références rvalue, c'est plus ou moins vrai.
Un des gros avantages pour le constructeur de déplacement et d'assignation de déplacement, c'est que si vous les définissez, le compilateur peut utiliser dans les cas ont été RVO (valeur de retour d'optimisation) et NRVO (nommé en valeur de retour d'optimisation) ne parviennent pas à être invoquée. C'est assez énorme pour le retour des objets coûteux comme des conteneurs & chaînes par valeur efficace de méthodes.
Maintenant là où les choses deviennent intéressantes, avec des références rvalue, c'est que vous pouvez également les utiliser comme arguments des fonctions normales. Cela permet d'écrire des conteneurs qui ont des surcharges pour les deux const référence (const foo& autre) et de la référence rvalue (foo&& autre). Même si l'argument est trop lourd pour passer avec un simple appel de constructeur il peut encore être fait:
Les conteneurs STL ont été mise à jour pour déplacer les surcharges pour presque rien (clé de hachage et des valeurs, vecteur d'insertion, etc), et c'est là que vous verrez le plus.
Vous pouvez également les utiliser pour les fonctions normales, et si vous n'en fournissez une référence rvalue argument, vous pouvez forcer l'appelant à créer l'objet et de laisser la fonction de faire 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 à tous les chargés de ressources, de sorte qu'il est plus facile pour voir ce que chaque objet représente dans le débogueur. L'interface est quelque chose comme ceci:
C'est une forme de "abstraction qui fuit", mais me permet de profiter du fait que j'ai eu à créer la chaîne déjà la plupart du temps, et éviter de faire encore une autre copie d'elle. Ce n'est pas exactement de haute performance code mais c'est un bon exemple des possibilités de gens obtenir le blocage de cette fonction. Ce code exige en fait que la variable soit de manière temporaire à l'appel, ou std::move invoquée:
ou
ou
mais cela ne compile pas!
Pas une réponse soi, mais une ligne directrice. La plupart du temps il n'y a pas beaucoup de sens en déclarant local
T&&
variable (comme vous l'avez fait avecstd::vector<int>&& rval_ref
). Vous aurez toujours àstd::move()
à utiliser dansfoo(T&&)
type de méthodes. Il y a aussi le problème qui a déjà été mentionné que lorsque vous essayez de les renvoyerrval_ref
à partir de la fonction, vous obtiendrez la norme de référence-à-détruit-temporaire-fiasco.La plupart du temps, j'irais avec le motif suivant:
Vous ne possédez pas toutes les refs du retour des objets temporaires, ainsi vous éviter (inexpérimenté) du programmeur d'erreur qui souhaitent utiliser un objet déplacé.
Évidemment il y a (bien qu'assez rares) cas où une fonction vraiment renvoie une
T&&
qui est une référence à un non-temporaire objet que vous pouvez vous déplacer dans votre objet.Concernant RVO: ces mécanismes fonctionnent en général et le compilateur peut bien éviter de copier, mais dans les cas où la voie de retour n'est pas évident (pour les exceptions,
if
conditions de la détermination de l'objet nommé, vous retournez, et probablement quelques autres) rrefs sont vos sauveurs (même si potentiellement plus cher).Aucun de ceux qui vont le faire supplémentaire de la copie. Même si RVO n'est pas utilisé, la nouvelle norme dit que déplacer la construction est préféré à la copie lorsque vous faites des retours je crois.
Je crois que votre deuxième exemple provoque un comportement indéfini, mais parce que vous êtes de retour d'une référence à une variable locale.
Comme déjà mentionné dans les commentaires à la première réponse, la
return std::move(...);
construction peut faire une différence dans d'autres cas que le retour de variables locales. Voici un exécutable exemple que les documents qu'advient-il lorsque vous revenez à un membre de l'objet avec et sansstd::move()
:Sans doute,
return std::move(some_member);
n'a de sens que si vous voulez vraiment déplacer le particulier membre de la classe, par exemple dans un cas oùclass C
représente de courte durée de l'adaptateur d'objets avec le seul but de créer des instances destruct A
.Avis comment
struct A
obtient toujours copié declass B
, même lorsque leclass B
objet est une R-valeur. C'est parce que le compilateur n'a aucun moyen de dire queclass B
s'instance destruct A
ne sera plus utilisée. Dansclass C
, le compilateur n'ont cette information à partir destd::move()
, c'est pourquoistruct A
obtient déplacé, à moins que l'instance declass C
est constante.