web-dev-qa-db-fra.com

Est-il possible de std :: déplacer des variables de pile locales?

Veuillez considérer le code suivant:

struct MyStruct
{
    int iInteger;
    string strString;
};

void MyFunc(vector<MyStruct>& vecStructs)
{
    MyStruct NewStruct = { 8, "Hello" };
    vecStructs.Push_back(std::move(NewStruct));
}

int main()
{
    vector<MyStruct> vecStructs;
    MyFunc(vecStructs);
}

Pourquoi ça marche?

Au moment où MyFunc est appelé, l'adresse de retour doit être placée sur la pile du thread actuel. Maintenant, créez l'objet NewStruct qui est créé, qui doit également être placé sur la pile. Avec std :: move, je dis au compilateur que je ne prévois plus d'utiliser la référence NewStruct. Il peut voler la mémoire. (La fonction Push_back est celle avec la sémantique de déplacement.)

Mais lorsque la fonction revient et que NewStruct tombe hors de portée. Même si le compilateur ne supprime pas la mémoire occupée par la structure existante à l'origine de la pile, il doit au moins supprimer l'adresse de retour précédemment stockée.

Cela conduirait à une pile fragmentée et les allocations futures remplaceraient la mémoire "déplacée".

Est ce que quelqu'un pourrait m'expliquer ça, s'il vous plait?


EDIT: Tout d'abord: merci beaucoup pour vos réponses. Mais d'après ce que j'ai appris, je ne comprends toujours pas pourquoi les éléments suivants ne fonctionnent pas comme je m'attends à ce que cela fonctionne:

struct MyStruct
{
    int iInteger;
    string strString;
    string strString2;
};

void MyFunc(vector<MyStruct>& vecStructs)
{
    MyStruct oNewStruct = { 8, "Hello", "Definetly more than 16 characters" };
    vecStructs.Push_back(std::move(oNewStruct));

    // At this point, oNewStruct.String2 should be "", because its memory was stolen.
    // But only when I explicitly create a move-constructor in the form which was
    // stated by Yakk, it is really that case.
}

void main()
{
    vector<MyStruct> vecStructs;
    MyFunc(vecStructs);
}
20
Allgaeuer

Premier, std::move ne bouge pas et std::forward ne transmet pas.

std::move est un transtypage en une référence rvalue. Par convention, les références rvalue sont traitées comme "des références dont vous êtes autorisé à retirer les données, car l'appelant promet qu'il n'a vraiment plus besoin de ces données".

De l'autre côté de la clôture, les références rvalue se lient implicitement à la valeur de retour de std::move (et parfois vers l'avant), vers des objets temporaires, dans certains cas lors du retour d'un local à partir d'une fonction, et lors de l'utilisation d'un membre d'un objet temporaire ou déplacé.

Ce qui se passe dans la fonction prenant une référence rvalue n'est pas magique. Il ne peut pas revendiquer le stockage directement dans l'objet en question. Il peut cependant lui arracher les tripes; il a la permission (par convention) de jouer avec l'état interne de ses arguments s'il peut faire l'opération plus rapidement de cette façon.

Maintenant, C++ écrira automatiquement quelques constructeurs de déplacement pour vous.

struct MyStruct
{
  int iInteger;
  string strString;
};

Dans ce cas, il écrira quelque chose qui ressemble à peu près à ceci:

MyStruct::MyStruct( MyStruct&& other ) noexcept(true) :
  iInteger( std::move(other.iInteger) ),
  strString( std::move(other.strString) )
{}

C'est-à-dire, il fera une construction de déplacement élément par élément.

Lorsque vous déplacez un entier, rien d'intéressant ne se produit. Il n'y a aucun avantage à jouer avec l'état de l'entier source.

Lorsque vous déplacez un std::string, nous obtenons des gains d'efficacité. La norme C++ décrit ce qui se passe lorsque vous passez d'un std::string à un autre. Fondamentalement, si la source std::string utilise le tas, le stockage du tas est transféré vers la destination std::string.

Il s'agit d'un modèle général de conteneurs C++; lorsque vous vous en éloignez, ils volent le stockage "alloué par tas" du conteneur source et le réutilisent dans la destination.

Notez que la source std::string demeure un std::string, juste un qui a les "tripes arrachées". La plupart des conteneurs comme les choses restent vides, je ne me souviens pas si std::string fait cette garantie (ce n'est peut-être pas dû à SBO), et ce n'est pas important pour le moment.

En bref, lorsque vous passez de quelque chose, sa mémoire n'est pas "réutilisée", mais la mémoire qu'elle possède peut être réutilisée.

Dans votre cas, MyStruct a un std::string qui peut utiliser la mémoire allouée en tas. Cette mémoire allouée en tas peut être déplacée dans le MyStruct stocké dans le std::vector.

En allant un peu plus loin dans le terrier du lapin, "Hello" est susceptible d'être si court que SBO se produit (petite optimisation de la mémoire tampon), et le std::string n'utilise pas du tout le tas. Dans ce cas particulier, il peut n'y avoir pratiquement aucune amélioration des performances en raison de moveing.

26

Votre exemple peut être réduit à:

vector<string> vec;
string str; // populate with a really long string
vec.Push_back(std::move(str));

Cela soulève toujours la question: "Est-il possible de déplacer des variables de pile locales." Il supprime simplement du code superflu pour le rendre plus facile à comprendre.

La réponse est oui. Un code comme celui-ci peut bénéficier de std::move Car std::string - au moins si le contenu est suffisamment volumineux - stocke les données réelles sur le tas, même si la variable est sur la pile.

Si vous n'utilisez pas std::move(), vous pouvez vous attendre à ce que du code tel que ci-dessus copie le contenu de str, qui peut être arbitrairement volumineux. Si vous utilisez std::move(), seuls les membres directs de la chaîne seront copiés (le déplacement n'a pas besoin de "mettre à zéro" les anciens emplacements), et les données seront utilisées sans modification ni copie.

C'est essentiellement la différence entre cela:

char* str; // populate with a really long string
char* other = new char[strlen(str)+1];
strcpy(other, str);

contre

char* str; // populate with a really long string
char* other = str;

Dans les deux cas, les variables sont sur la pile. Mais les données ne le sont pas.

Si vous avez un cas où vraiment toutes les données sont sur la pile, comme un std::string Avec la "petite optimisation de chaîne" en vigueur, ou une structure contenant des entiers, alors std::move() achètera vous rien.

10
John Zwinck