C++11 rvalues et confusion sémantique de déplacement (déclaration 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 va pas faire 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();
5 réponses
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 intercepté par rval_ref
. Ce temporaire aura sa vie prolongée au-delà de la définition rval_ref
et vous pouvez l'utiliser comme si vous l'aviez attrapé 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
de manière non-const.
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
détient maintenant un référence au tmp
détruit à l'intérieur de la fonction. Avec un peu de chance, ce code se bloque 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 à votre premier. Le std::move
sur {[9] } est inutile et peut en fait être une pessimisation des performances car il inhibera l'optimisation de la valeur de retour.
La meilleure façon de coder ce que vous faites est:
Meilleures pratiques
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 un rvalue dans l'instruction return. Il sera retourné via return-value-optimization (pas de copie, pas de mouvement), ou si le compilateur décide qu'il ne peut pas effectuer RVO, il utilisera le constructeur move de vector pour faire le retour. Ce n'est que si RVO n'est pas exécuté et si le type renvoyé n'a pas de constructeur move que le constructeur copy sera utilisé pour le retour.
Aucun d'entre eux ne copiera, mais le second se référera à un vecteur détruit. Les références rvalue nommées n'existent presque jamais dans le code normal. Vous l'écrivez juste 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 maintenant, le vecteur est déplacé. L'utilisateur d'une classe ne gère pas ses références rvalue dans la grande majorité des cas.
La réponse simple est que vous devriez écrire du code pour les références rvalue comme vous le feriez pour le code de références régulières, et vous devriez les traiter de la même manière mentalement 99% du temps. Cela inclut toutes les anciennes règles sur le renvoi de références (c'est-à-dire Ne jamais renvoyer une 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 qui prend des références lvalue ou rvalue, c'est plus ou moins de moins en moins vrai.
L'un des grands avantages du constructeur move et de l'affectation move est que si vous les définissez, le compilateur peut les utiliser dans les cas où le RVO (return value optimization) et le NRVO (named return value optimization) ne peuvent pas être invoqués. C'est assez énorme pour renvoyer des objets coûteux comme des conteneurs et des chaînes par valeur efficacement à partir de méthodes.
Maintenant, où les choses deviennent intéressantes avec les références rvalue, c'est que vous pouvez également les utiliser comme arguments à la normale fonction. 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 lourd pour passer avec un simple appel de constructeur il peut encore ê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 avoir des surcharges de déplacement pour presque n'importe quoi (clé de hachage et valeurs, 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 faire 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, de sorte qu'il est plus facile de voir ce que chaque objet représente dans le débogueur. L'interface est quelque chose comme 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 qui me permet de profiter du fait que j'ai dû 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 du code haute performance, mais c'est un bon exemple des possibilités que les gens obtiennent le coup de cette fonctionnalité. Ce code nécessite en fait que la variable soit temporaire à l'appel, ou std:: move 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 compile pas!
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);
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érée à copier lors des retours, je crois.
Je crois que votre deuxième exemple provoque un comportement indéfini parce que vous renvoyez une référence à une variable locale.
Pas une réponse soi, mais une ligne directrice. La plupart du temps, il n'y a pas beaucoup de sens à déclarer la variable locale T&&
(comme vous l'avez fait avec std::vector<int>&& rval_ref
). Vous devrez toujours les {[4] } à utiliser dans les méthodes de type foo(T&&)
. Il y a aussi le problème qui a déjà été mentionné que lorsque vous essayez de retourner un tel rval_ref
de la fonction, vous obtiendrez la référence standard-to-destroyed-temporary-fiasco.
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 n'en avez pas refs aux objets temporaires retournés, vous évitez ainsi l'erreur du programmeur (inexpérimenté) 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();
Évidemment, il y a (bien que plutôt rare) des cas où une fonction renvoie vraiment un T&&
qui est une référence à un objetnon 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
conditionnels déterminant l'objet nommé que vous retournerez, et probablement en couple d'autres) les rrefs sont vos sauveurs (même s'ils sont potentiellement plus chers).