519 votes

C++11 rvalues et sémantique de déplacement avec l'instruction return

Je cherche à 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 lequel d'entre eux ne va pas effectuer de copie du vecteur?

Premier exemple:

std::vector return_vector(void)
{
    std::vector tmp {1,2,3,4,5};
    return tmp;
}

std::vector &&rval_ref = return_vector();

Deuxième exemple:

std::vector&& return_vector(void)
{
    std::vector tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector &&rval_ref = return_vector();

Troisième exemple:

std::vector return_vector(void)
{
    std::vector tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector &&rval_ref = return_vector();

59 votes

Veuillez ne jamais retourner de variables locales par référence. Une référence à un rvalue est toujours une référence.

73 votes

C'était évidemment intentionnel pour comprendre les différences sémantiques entre les exemples lol

0 votes

@FredOverflow Ancienne question, mais il m'a fallu une seconde pour comprendre votre commentaire. Je pense que la question avec #2 était de savoir si std::move() créait une "copie" persistante.

660voto

Howard Hinnant Points 59526

Premier exemple

std::vector return_vector(void)
{
    std::vector tmp {1,2,3,4,5};
    return tmp;
}

std::vector &&rval_ref = return_vector();

Le premier exemple renvoie un temporaire qui est capturé par rval_ref. Ce temporaire aura sa vie prolongée au-delà de la définition de rval_ref et vous pouvez l'utiliser comme si vous l'aviez capturé par valeur. C'est très similaire à ce qui suit:

const std::vector& rval_ref = return_vector();

sauf que dans ma réécriture vous ne pouvez évidemment pas utiliser rval_ref de manière non constante.

Deuxième exemple

std::vector&& return_vector(void)
{
    std::vector tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector &&rval_ref = return_vector();

Dans le second exemple, vous avez créé une erreur d'exécution. rval_ref détient maintenant une référence du tmp destructuré à l'intérieur de la fonction. Avec un peu de chance, ce code crashera immédiatement.

Troisième exemple

std::vector return_vector(void)
{
    std::vector tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector &&rval_ref = return_vector();

Votre troisième exemple est approximativement équivalent au premier. Le std::move sur tmp est inutile et peut en fait être une pessimization de performance car il inhibera l'optimisation de la valeur de retour.

La meilleure façon de coder ce que vous faites est:

Meilleure pratique

std::vector return_vector(void)
{
    std::vector tmp {1,2,3,4,5};
    return tmp;
}

std::vector rval_ref = return_vector();

C'est-à-dire, juste comme vous le feriez en C++03. tmp est implicitement traité comme un rvalue dans l'instruction de retour. 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 effectuer RVO, alors il utilisera le constructeur de déplacement du vecteur pour effectuer le retour. Seulement si RVO n'est pas effectuée, et si le type retourné n'avait pas de constructeur de déplacement, le constructeur de copie serait utilisé pour le retour.

0 votes

Ainsi, d'après ce que je comprends, la meilleure chose à faire est que les objets aient un constructeur de déplacement. Je devrais probablement juste chercher sur Google, mais je suis un peu paresseux en ce moment; y a-t-il des directives courantes pour les compilateurs sur le RVO ?

78 votes

Les compilateurs effectueront une optimisation de la valeur de retour (RVO) lorsque vous retournez un objet local par valeur, et que le type du local et le type de retour de la fonction sont identiques, et aucun des deux n'est qualifié par constante (ne renvoyez pas de types const). Évitez de renvoyer avec l'instruction (:?) car cela peut entraver l'optimisation RVO. Ne placez pas l'objet local dans une autre fonction qui renvoie une référence à l'objet local. Il suffit de retourner mon_local;. Plusieurs instructions de retour sont acceptables et n'empêcheront pas l'optimisation RVO.

39 votes

Il y a un avertissement : lorsqu’on retourne un membre d’un objet local, le déplacement doit être explicite.

48voto

Puppy Points 90818

Aucun d'entre eux ne sera copié, mais le deuxième fera référence à un vecteur détruit. Nommées références rvalue, elles existent presque jamais dans le code régulier. Vous l'écrivez exactement comme vous l'auriez écrit une copie en C++03.

std::vector return_vector()
{
    std::vector tmp {1,2,3,4,5};
    return tmp;
}

std::vector rval_ref = return_vector();

Sauf que maintenant, le vecteur est déplacé. L'utilisateur d'une classe ne traite pas ses références rvalue dans la grande majorité des cas.

0 votes

Êtes-vous vraiment sûr que le troisième exemple va effectuer une copie de vecteur ?

0 votes

@Tarantula: Cela va défoncer votre vecteur. Que cela l'ait copié avant de se briser ou pas n'a pas vraiment d'importance.

4 votes

Je ne vois aucune raison pour laquelle l'explosion que vous proposez serait nécessaire. Il est tout à fait correct de lier une variable de référence rvalue locale à une rvalue. Dans ce cas, la durée de vie de l'objet temporaire est prolongée jusqu'à la durée de vie de la variable de référence rvalue.

18voto

Zoner Points 386

La réponse simple est que vous devez écrire du code pour les références rvalue comme vous le feriez pour du code de références régulières, et vous devriez les traiter mentalement de la même manière 99 % du temps. Cela inclut toutes les anciennes règles concernant le renvoi de références (c'est-à-dire ne jamais renvoyer une référence à une variable locale).

À moins que vous n'écriviez une classe de conteneur modèle qui doit tirer parti de std::forward et être en mesure d'écrire une fonction générique qui prend soit des références lvalue, soit des références rvalue, c'est plus ou moins vrai.

Un des grands avantages du constructeur de déplacement et de l'assignation de déplacement est que si vous les définissez, le compilateur peut les utiliser dans les cas où l'optimisation du retour de valeur (RVO) et l'optimisation du retour de valeur nommée (NRVO) ne sont pas invoquées. C'est très important pour le retour efficace par valeur d'objets coûteux comme des conteneurs et des chaînes depuis des méthodes.

Maintenant, là où les choses deviennent intéressantes avec les références rvalue, c'est que vous pouvez aussi les utiliser comme arguments pour des fonctions normales. Cela vous permet d'écrire des conteneurs avec des surcharges à la fois pour la référence constante (const foo& other) et la référence rvalue (foo&& other). Même si l'argument est trop difficile à passer avec un simple appel de constructeur, cela peut quand même être fait :

std::vector vec;
for(int x=0; x<10; ++x)
{
    // utilise automatiquement le constructeur de référence rvalue si disponible
    // car MyCheapType est une variable temporaire sans nom
    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));

    // ancienne méthode, passée via référence constante, copie coûteuse
    vec.push_back(temp);

    // nouvelle méthode, passée via référence rvalue, déplacement bon marché
    // il suffit de ne pas réutiliser temp, pas difficile dans une boucle comme celle-ci cependant . . .
    vec.push_back(std::move(temp));
}

Les conteneurs STL ont été mis à jour pour avoir des surcharges de déplacement pour presque tout (clés et valeurs de hachage, insertion de vecteur, etc), et c'est là que vous les verrez le plus souvent.

Vous pouvez aussi les utiliser pour des fonctions normales, et si vous ne fournissez qu'un argument de référence rvalue, vous pouvez contraindre l'appelant à créer l'objet et laisser la fonction effectuer le déplacement. Il s'agit plus d'un exemple que d'un véritable bon usage, mais dans ma bibliothèque de rendu, j'ai attribué une chaîne à toutes les ressources chargées, pour qu'il soit plus facile de voir ce que chaque objet représente dans le débogueur. L'interface ressemble à ceci :

TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
{
    std::unique_ptr tex = D3DCreateTexture(width, height, fmt);
    tex->friendlyName = std::move(friendlyName);
    return tex;
}

C'est une forme d'« abstraction défectueuse » mais me permet de tirer parti du fait que j'ai déjà dû créer la chaîne la plupart du temps, et d'éviter de la recopier une fois de plus. Ce n'est pas exactement un code haute performance mais c'est un bon exemple des possibilités lorsque les gens prennent l'habitude de cette fonctionnalité. Ce code exige en fait que la variable soit soit un temporaire de l'appel, soit que std::move soit invoqué :

// déplacer à partir d'un temporaire
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));

ou

// déplacement explicite (on ne va pas utiliser la variable 'str' après l'appel create)
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));

ou

// fabriquer explicitement une copie et passer le temporaire de la copie en bas
// car nous devons utiliser str à nouveau pour une raison quelconque
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));

mais cela ne compilera pas !

string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);

4voto

Red XIII Points 1247

Pas une réponse en soi, mais une ligne directrice. La plupart du temps, il n'y a pas grand sens à déclarer une variable locale de type T&& (comme vous l'avez fait avec std::vector&& rval_ref). Vous devrez quand même les passer à std::move() pour les utiliser dans des méthodes de type foo(T&&). Il y a aussi le problème qui a déjà été mentionné : lorsque vous essayez de retourner rval_ref d'une fonction, vous obtiendrez le fiasco standard de la référence à un temporaire détruit.

La plupart du temps, j'irais avec le schéma suivant :

// Déclarations
A a(B&&, C&&);
B b();
C c();

auto ret = a(b(), c());

Vous ne retenez aucune référence aux objets temporaires retournés, évitant ainsi l'erreur d'un programmeur (peu expérimenté) qui souhaite utiliser un objet déplacé.

auto bRet = b();
auto cRet = c();
auto aRet = a(std::move(b), std::move(c));

// Soit ces appels échouent simplement (assertion/exception), soit vous n'obtiendrez pas 
// les résultats escomptés en raison de leur état propre.
bRet.foo();
cRet.bar();

Évidemment, il existe (bien que plutôt rares) des cas où une fonction retourne réellement un T&& qui est une référence à un objet non temporaire que vous pouvez déplacer dans votre objet.

Concernant le RVO : ces mécanismes fonctionnent généralement bien et le compilateur peut éviter élégamment les copies, mais dans les cas où le chemin de retour n'est pas évident (exceptions, conditions if déterminant l'objet nommé que vous retournerez, et probablement quelques autres), les rrefs sont vos sauveurs (même s'ils sont potentiellement plus coûteux).

3voto

Crazy Eddie Points 23778

Aucun de ceux-ci ne fera de copie supplémentaire. Même si RVO n'est pas utilisé, le nouveau standard dit que la construction de déplacement est préférée à la copie lors des retours je crois.

Je crois que votre deuxième exemple provoque cependant un comportement indéfini car vous retournez une référence à une variable locale.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X