Qu'est-ce que la sémantique des mouvements?

1706

Je viens de finir d'écouter l' interview de podcast radio de Software Engineering avec Scott Meyers concernant C ++ 0x . La plupart des nouvelles fonctionnalités avaient du sens pour moi, et je suis vraiment enthousiasmé par C ++ 0x maintenant, à l'exception d'une. Je n'ai toujours pas de sémantique de mouvement ... Qu'est-ce que c'est exactement?

dicroce
la source
20
J'ai trouvé [l'article de blog d'Eli Bendersky] ( eli.thegreenplace.net/2011/12/15/… ) sur les lvalues ​​et les rvalues ​​en C et C ++ assez informatif. Il mentionne également les références rvalue en C ++ 11 et les présente avec de petits exemples.
Nils
16
L'exposition d'Alex Allain sur le sujet est très bien écrite.
Patrick Sanan
19
Chaque année, je me demande en quoi consiste la "nouvelle" sémantique de déplacement en C ++, je la recherche sur Google et j'arrive sur cette page. Je lis les réponses, mon cerveau s'arrête. Je retourne en C, et j'oublie tout! Je suis dans l'impasse.
ciel le
7
@sky Considérez std :: vector <> ... Quelque part, il y a un pointeur vers un tableau sur le tas. Si vous copiez cet objet, un nouveau tampon doit être alloué et les données du tampon doivent être copiées dans le nouveau tampon. Y a-t-il des circonstances où il serait OK de simplement voler le pointeur? La réponse est OUI, lorsque le compilateur sait que l'objet est temporaire. La sémantique de déplacement vous permet de définir comment les entrailles de vos classes peuvent être déplacées et déposées dans un autre objet lorsque le compilateur sait que l'objet à partir duquel vous vous déplacez est sur le point de disparaître.
dicroce
La seule référence que je peux comprendre: learncpp.com/cpp-tutorial/… , c'est-à-dire que le raisonnement original de la sémantique des mouvements provient de pointeurs intelligents.
jw_

Réponses:

2484

Je trouve qu'il est plus facile de comprendre la sémantique des mouvements avec un exemple de code. Commençons par une classe de chaînes très simple qui ne contient qu'un pointeur vers un bloc de mémoire alloué par tas:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

Puisque nous avons choisi de gérer la mémoire nous-mêmes, nous devons suivre la règle des trois . Je vais différer l'écriture de l'opérateur d'affectation et n'implémenter que le destructeur et le constructeur de copie pour l'instant:

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

Le constructeur de copie définit ce que signifie copier des objets chaîne. Le paramètre const string& thatse lie à toutes les expressions de type chaîne qui vous permet de faire des copies dans les exemples suivants:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

Voici maintenant la clé de la sémantique des mouvements. Notez que ce n'est que dans la première ligne où nous copions que xcette copie profonde est vraiment nécessaire, car nous pourrions vouloir inspecter xplus tard et serions très surpris si elle xavait changé d'une manière ou d'une autre. Avez-vous remarqué comment je viens de dire xtrois fois (quatre fois si vous incluez cette phrase) et signifiait exactement le même objet à chaque fois? Nous appelons des expressions telles que x"lvalues".

Les arguments des lignes 2 et 3 ne sont pas des lvalues, mais des rvalues, car les objets de chaîne sous-jacents n'ont pas de nom, le client n'a donc aucun moyen de les inspecter à nouveau ultérieurement. rvalues ​​désigne des objets temporaires qui sont détruits au point-virgule suivant (pour être plus précis: à la fin de l'expression complète qui contient lexicalement la rvalue). Ceci est important car lors de l'initialisation de bet c, nous pouvions faire ce que nous voulions avec la chaîne source, et le client ne pouvait pas faire la différence !

C ++ 0x introduit un nouveau mécanisme appelé "référence rvalue" qui, entre autres, nous permet de détecter les arguments rvalue via une surcharge de fonction. Tout ce que nous avons à faire est d'écrire un constructeur avec un paramètre de référence rvalue. À l'intérieur de ce constructeur, nous pouvons faire tout ce que nous voulons avec la source, tant que nous le laissons dans un état valide:

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

Qu'avons-nous fait ici? Au lieu de copier en profondeur les données du tas, nous venons de copier le pointeur, puis de définir le pointeur d'origine sur null (pour empêcher «supprimer []» du destructeur de l'objet source de libérer nos «données juste volées»). En effet, nous avons «volé» les données qui appartenaient à l'origine à la chaîne source. Encore une fois, l'idée clé est qu'en aucun cas le client n'a pu détecter que la source avait été modifiée. Puisque nous ne faisons pas vraiment de copie ici, nous appelons ce constructeur un "constructeur de déplacement". Son travail consiste à déplacer des ressources d'un objet à un autre au lieu de les copier.

Félicitations, vous comprenez maintenant les bases de la sémantique des mouvements! Continuons en implémentant l'opérateur d'affectation. Si vous n'êtes pas familier avec l' idiome de copie et d'échange , apprenez-le et revenez, car c'est un idiome C ++ génial lié à la sécurité des exceptions.

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

Hein, c'est ça? "Où est la référence rvalue?" vous pourriez demander. "Nous n'en avons pas besoin ici!" est ma réponse :)

Notez que nous transmettons le paramètre that par valeur , il thatdoit donc être initialisé comme tout autre objet chaîne. Comment thatva- t-on exactement être initialisé? Dans les temps anciens de C ++ 98 , la réponse aurait été "par le constructeur de copie". En C ++ 0x, le compilateur choisit entre le constructeur de copie et le constructeur de déplacement selon que l'argument de l'opérateur d'affectation est une lvalue ou une rvalue.

Donc, si vous dites a = b, le constructeur de copie s'initialise that(car l'expression best une valeur l), et l'opérateur d'affectation échange le contenu avec une copie complète fraîchement créée. C'est la définition même de l'idiome de copie et d'échange - faites une copie, échangez le contenu avec la copie, puis débarrassez-vous de la copie en quittant la portée. Rien de nouveau ici.

Mais si vous dites a = x + y, le constructeur de mouvement s'initialise that(car l'expression x + yest une valeur r), donc il n'y a pas de copie profonde impliquée, seulement un déplacement efficace. thatest toujours un objet indépendant de l'argument, mais sa construction était triviale, car les données de tas n'avaient pas à être copiées, juste déplacées. Il n'était pas nécessaire de le copier car x + yc'est une rvalue, et encore une fois, il est correct de se déplacer à partir d'objets chaîne dénotés par rvalues.

Pour résumer, le constructeur de copie effectue une copie complète, car la source doit rester intacte. Le constructeur de déplacement, d'autre part, peut simplement copier le pointeur, puis définir le pointeur dans la source sur null. Il est correct de "annuler" l'objet source de cette manière, car le client n'a aucun moyen d'inspecter l'objet à nouveau.

J'espère que cet exemple a permis de faire passer le message principal. Il y a beaucoup plus à réévaluer les références et à déplacer la sémantique que j'ai intentionnellement omis pour rester simple. Si vous souhaitez plus de détails, veuillez consulter ma réponse supplémentaire .

fredoverflow
la source
40
@Mais si mon ctor obtient une valeur r, qui ne pourra jamais être utilisée plus tard, pourquoi dois-je même prendre la peine de la laisser dans un état cohérent / sûr? Au lieu de mettre that.data = 0, pourquoi ne pas le laisser?
einpoklum
70
@einpoklum Parce que sans that.data = 0, les personnages seraient détruits bien trop tôt (à la mort temporaire), et aussi deux fois. Vous voulez voler les données, pas les partager!
fredoverflow
19
@einpoklum Le destructeur régulièrement planifié s'exécute toujours, vous devez donc vous assurer que l'état post-déplacement de l'objet source ne provoque pas de plantage. Mieux, vous devez vous assurer que l'objet source peut également être le destinataire d'une affectation ou d'une autre écriture.
CTMacUser
12
@pranitkothari Oui, tous les objets doivent être détruits, même les objets déplacés. Et comme nous ne voulons pas que le tableau char soit supprimé lorsque cela se produit, nous devons définir le pointeur sur null.
fredoverflow
7
@ Virus721 delete[]sur un nullptr est défini par le standard C ++ comme un no-op.
fredoverflow
1059

Ma première réponse a été une introduction extrêmement simplifiée pour déplacer la sémantique, et de nombreux détails ont été laissés exprès pour rester simple. Cependant, il y a beaucoup plus à faire pour déplacer la sémantique, et je pensais qu'il était temps pour une deuxième réponse de combler les lacunes. La première réponse est déjà assez ancienne et il ne semblait pas juste de la remplacer simplement par un texte complètement différent. Je pense que cela sert toujours bien de première introduction. Mais si vous voulez approfondir, lisez la suite :)

Stephan T. Lavavej a pris le temps de fournir de précieux commentaires. Merci beaucoup, Stephan!

introduction

La sémantique de déplacement permet à un objet, sous certaines conditions, de s'approprier les ressources externes d'un autre objet. Ceci est important de deux manières:

  1. Transformer des copies coûteuses en mouvements bon marché. Voir ma première réponse pour un exemple. Notez que si un objet ne gère pas au moins une ressource externe (directement ou indirectement via ses objets membres), la sémantique de déplacement n'offre aucun avantage par rapport à la sémantique de copie. Dans ce cas, copier un objet et déplacer un objet signifie exactement la même chose:

    class cannot_benefit_from_move_semantics
    {
        int a;        // moving an int means copying an int
        float b;      // moving a float means copying a float
        double c;     // moving a double means copying a double
        char d[64];   // moving a char array means copying a char array
    
        // ...
    };
  2. Implémentation de types sécurisés «de déplacement uniquement»; c'est-à-dire les types pour lesquels la copie n'a pas de sens, mais le déplacement le fait. Les exemples incluent les verrous, les descripteurs de fichiers et les pointeurs intelligents avec une sémantique de propriété unique. Remarque: Cette réponse traite d' std::auto_ptrun modèle de bibliothèque standard C ++ 98 obsolète, qui a été remplacé par std::unique_ptrC ++ 11. Les programmeurs C ++ intermédiaires sont probablement au moins assez familiers et std::auto_ptr, en raison de la "sémantique de déplacement" qu'il affiche, cela semble être un bon point de départ pour discuter de la sémantique de déplacement en C ++ 11. YMMV.

Qu'est-ce qu'un déménagement?

La bibliothèque standard C ++ 98 propose un pointeur intelligent avec une sémantique de propriété unique appelée std::auto_ptr<T>. Si vous n'êtes pas familier auto_ptr, son but est de garantir qu'un objet alloué dynamiquement est toujours libéré, même en cas d'exceptions:

{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically

Ce qui est inhabituel, auto_ptrc'est son comportement de "copie":

auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

Notez que l'initialisation de bwith ane copie pas le triangle, mais transfère à la place la propriété du triangle de aà b. Nous disons également " aest déplacé vers b " ou "le triangle est déplacé de a vers b ". Cela peut sembler déroutant car le triangle lui-même reste toujours au même endroit dans la mémoire.

Déplacer un objet signifie transférer la propriété d'une ressource qu'il gère à un autre objet.

Le constructeur de copie de auto_ptrressemble probablement à quelque chose comme ceci (quelque peu simplifié):

auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}

Mouvements dangereux et inoffensifs

Ce qui est dangereux, auto_ptrc'est que ce qui ressemble syntaxiquement à une copie est en fait un mouvement. Essayer d'appeler une fonction membre sur un élément déplacé auto_ptrinvoquera un comportement indéfini, vous devez donc être très prudent de ne pas utiliser un élément auto_ptraprès son déplacement:

auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior

Mais ce auto_ptrn'est pas toujours dangereux. Les fonctions d'usine sont un cas d'utilisation parfaitement adapté pour auto_ptr:

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

Notez comment les deux exemples suivent le même modèle syntaxique:

auto_ptr<Shape> variable(expression);
double area = expression->area();

Et pourtant, l'un d'eux invoque un comportement indéfini, tandis que l'autre ne le fait pas. Quelle est donc la différence entre les expressions aet make_triangle()? Ne sont-ils pas tous les deux du même type? En effet, ils le sont, mais ils ont des catégories de valeurs différentes .

Catégories de valeur

De toute évidence, il doit y avoir une différence profonde entre l'expression aqui dénote une auto_ptrvariable et l'expression make_triangle()qui dénote l'appel d'une fonction qui renvoie une auto_ptrvaleur par, créant ainsi un nouvel auto_ptrobjet temporaire à chaque appel. aest un exemple de valeur l , alors que make_triangle()c'est un exemple de valeur r .

Passer de lvalues ​​telles que aest dangereux, car nous pourrions plus tard essayer d'appeler une fonction membre via a, en invoquant un comportement non défini. D'un autre côté, passer de rvalues ​​telles que make_triangle()est parfaitement sûr, car une fois que le constructeur de copie a fait son travail, nous ne pouvons plus utiliser le temporaire. Il n'y a aucune expression qui dénote ledit temporaire; si nous écrivons simplement à make_triangle()nouveau, nous obtenons un autre temporaire. En fait, le temporaire déplacé est déjà parti sur la ligne suivante:

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here

Notez que les lettres let ront une origine historique dans le côté gauche et le côté droit d'une affectation. Ce n'est plus le cas en C ++, car il y a des valeurs qui ne peuvent pas apparaître sur le côté gauche d'une affectation (comme des tableaux ou des types définis par l'utilisateur sans opérateur d'affectation), et il y a des rvalues ​​qui peuvent (toutes les rvalues ​​des types de classe avec un opérateur d'affectation).

Une valeur r de type classe est une expression dont l'évaluation crée un objet temporaire. Dans des circonstances normales, aucune autre expression à l'intérieur de la même portée ne dénote le même objet temporaire.

Références de valeur

Nous comprenons maintenant que passer de lvalues ​​est potentiellement dangereux, mais passer de rvalues ​​est inoffensif. Si C ++ avait un support de langage pour distinguer les arguments lvalue des arguments rvalue, nous pourrions soit interdire complètement le déplacement à partir de lvalues, soit au moins rendre explicite le déplacement à partir de lvalues sur le site d'appel, afin de ne plus bouger par accident.

La réponse de C ++ 11 à ce problème est les références rvalue . Une référence rvalue est un nouveau type de référence qui se lie uniquement aux rvalues, et la syntaxe est X&&. La bonne vieille référence X&est maintenant connue sous le nom de référence lvalue . (Notez que ce X&&n'est pas une référence à une référence; il n'y a rien de tel en C ++.)

Si nous jetons constdans le mélange, nous avons déjà quatre types de références différents. À quels types d'expressions de type Xpeuvent-ils se lier?

            lvalue   const lvalue   rvalue   const rvalue
---------------------------------------------------------              
X&          yes
const X&    yes      yes            yes      yes
X&&                                 yes
const X&&                           yes      yes

En pratique, vous pouvez oublier const X&&. Restreindre à lire à partir des valeurs r n'est pas très utile.

Une référence rvalue X&&est un nouveau type de référence qui se lie uniquement aux rvalues.

Conversions implicites

Les références Rvalue sont passées par plusieurs versions. Depuis la version 2.1, une référence rvalue X&&se lie également à toutes les catégories de valeurs d'un type différent Y, à condition qu'il y ait une conversion implicite de Yà X. Dans ce cas, un temporaire de type Xest créé et la référence rvalue est liée à ce temporaire:

void some_function(std::string&& r);

some_function("hello world");

Dans l'exemple ci-dessus, "hello world"est une valeur l de type const char[12]. Puisqu'il y a une conversion implicite de const char[12]à const char*en std::string, un temporaire de type std::stringest créé et rest lié à ce temporaire. C'est l'un des cas où la distinction entre rvalues ​​(expressions) et temporelles (objets) est un peu floue.

Déplacer les constructeurs

Un exemple utile de fonction avec un X&&paramètre est le constructeur de déplacement X::X(X&& source) . Son objectif est de transférer la propriété de la ressource gérée de la source à l'objet actuel.

En C ++ 11, std::auto_ptr<T>a été remplacé par std::unique_ptr<T>qui tire parti des références rvalue. Je développerai et discuterai d'une version simplifiée de unique_ptr. Tout d'abord, nous encapsulons un pointeur brut et surchargeons les opérateurs ->et *, par conséquent, notre classe se sent comme un pointeur:

template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }

Le constructeur prend possession de l'objet et le destructeur le supprime:

    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }

Vient maintenant la partie intéressante, le constructeur de mouvement:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

Ce constructeur de déplacement fait exactement ce que le auto_ptrconstructeur de copie a fait, mais il ne peut être fourni qu'avec des rvalues:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay

La deuxième ligne ne parvient pas à compiler, car il as'agit d'une valeur l, mais le paramètre unique_ptr&& sourcene peut être lié qu'à des valeurs r. C'est exactement ce que nous voulions; les mouvements dangereux ne devraient jamais être implicites. La troisième ligne se compile très bien, car il make_triangle()s'agit d'une valeur r. Le constructeur du déménagement transférera la propriété du temporaire au c. Encore une fois, c'est exactement ce que nous voulions.

Le constructeur de déplacement transfère la propriété d'une ressource gérée dans l'objet actuel.

Déplacer les opérateurs d'affectation

La dernière pièce manquante est l'opérateur d'affectation de déplacement. Son travail consiste à libérer l'ancienne ressource et à acquérir la nouvelle ressource à partir de son argument:

    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};

Notez comment cette implémentation de l'opérateur d'affectation de déplacement duplique la logique du destructeur et du constructeur de déplacement. Connaissez-vous l'idiome de copie et d'échange? Il peut également être appliqué pour déplacer la sémantique en tant qu'idiome de déplacement et d'échange:

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

Maintenant que sourcec'est une variable de type unique_ptr, elle sera initialisée par le constructeur de déplacement; c'est-à-dire que l'argument sera déplacé dans le paramètre. L'argument doit toujours être une valeur r, car le constructeur de déplacement lui-même a un paramètre de référence rvalue. Lorsque le flux de contrôle atteint l'accolade de fermeture de operator=, sourcesort du champ d'application, libérant automatiquement l'ancienne ressource.

L'opérateur d'affectation de déplacement transfère la propriété d'une ressource gérée dans l'objet actuel, libérant l'ancienne ressource. L'idiome move-and-swap simplifie l'implémentation.

Passer de lvalues

Parfois, nous voulons passer de lvalues. Autrement dit, nous voulons parfois que le compilateur traite une valeur l comme s'il s'agissait d'une valeur r, de sorte qu'il peut invoquer le constructeur de déplacement, même s'il peut être potentiellement dangereux. À cette fin, C ++ 11 propose un modèle de fonction de bibliothèque standard appelé std::moveà l'intérieur de l'en-tête <utility>. Ce nom est un peu malheureux, car std::moveil jette simplement une valeur l à une valeur r; il ne pas bouger quoi que ce soit par lui - même. Il permet simplement de bouger. Peut-être qu'il aurait dû être nommé std::cast_to_rvalueou std::enable_move, mais nous sommes coincés avec le nom maintenant.

Voici comment vous passez explicitement d'une valeur l:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

Notez qu'après la troisième ligne, ane possède plus de triangle. Ce n'est pas grave, car en écrivant explicitementstd::move(a) , nous avons clairement exprimé nos intentions: "Cher constructeur, faites tout ce que vous voulez apour initialiser c; je m'en fiche aplus. N'hésitez pas à vous débrouiller a."

std::move(some_lvalue) convertit une lvalue en une rvalue, permettant ainsi un déplacement ultérieur.

Xvalues

Notez que même s'il std::move(a)s'agit d'une valeur r, son évaluation ne crée pas d'objet temporaire. Cette énigme a forcé le comité à introduire une troisième catégorie de valeur. Quelque chose qui peut être lié à une référence rvalue, même s'il ne s'agit pas d'une valeur r au sens traditionnel, est appelé une valeur x (valeur eXpiring). Les valeurs traditionnelles ont été renommées en valeurs (valeurs pures).

Les valeurs pr et x sont des valeurs r. Les valeurs X et les valeurs l sont toutes deux des valeurs gl (valeurs l généralisées). Les relations sont plus faciles à saisir avec un diagramme:

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues

Notez que seules les valeurs x sont vraiment nouvelles; le reste est simplement dû au changement de nom et au regroupement.

C ++ 98 rvalues ​​sont appelées prvalues ​​dans C ++ 11. Remplacez mentalement toutes les occurrences de "rvalue" dans les paragraphes précédents par "prvalue".

Sortir des fonctions

Jusqu'à présent, nous avons vu des mouvements dans des variables locales et dans des paramètres de fonction. Mais le déplacement est également possible en sens inverse. Si une fonction renvoie par valeur, un objet sur le site d'appel (probablement une variable locale ou temporaire, mais pourrait être n'importe quel type d'objet) est initialisé avec l'expression après l' returninstruction comme argument pour le constructeur de déplacement:

unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());

De manière surprenante, les objets automatiques (variables locales qui ne sont pas déclarées comme static) peuvent également être implicitement retirés des fonctions:

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

Comment se fait-il que le constructeur de mouvement accepte la valeur l resultcomme argument? La portée de resultest sur le point de se terminer et elle sera détruite lors du déroulement de la pile. Personne ne pouvait se plaindre par la suite qui resultavait changé d'une manière ou d'une autre; lorsque le flux de contrôle est de retour à l'appelant, resultn'existe plus! Pour cette raison, C ++ 11 a une règle spéciale qui permet de retourner des objets automatiques à partir de fonctions sans avoir à écrire std::move. En fait, vous ne devez jamais utiliser std::movepour déplacer des objets automatiques hors des fonctions, car cela inhibe l '"optimisation de la valeur de retour nommée" (NRVO).

Ne jamais utiliser std::movepour déplacer des objets automatiques hors des fonctions.

Notez que dans les deux fonctions d'usine, le type de retour est une valeur, pas une référence rvalue. Les références Rvalue sont toujours des références, et comme toujours, vous ne devez jamais renvoyer une référence à un objet automatique; l'appelant se retrouverait avec une référence pendant si vous incitiez le compilateur à accepter votre code, comme ceci:

unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}

Ne renvoyez jamais d'objets automatiques par référence rvalue. Le déplacement est exclusivement effectué par le constructeur de déplacement, pas par std::moveet pas simplement en liant une rvalue à une référence rvalue.

Entrer dans les membres

Tôt ou tard, vous allez écrire du code comme ceci:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

Fondamentalement, le compilateur se plaindra qu'il parameters'agit d'une valeur l. Si vous regardez son type, vous voyez une référence rvalue, mais une référence rvalue signifie simplement "une référence qui est liée à une rvalue"; cela ne signifie pas que la référence elle-même est une valeur! En effet, parameterc'est juste une variable ordinaire avec un nom. Vous pouvez utiliser parameterautant de fois que vous le souhaitez à l'intérieur du corps du constructeur, et il désigne toujours le même objet. S'en éloigner implicitement serait dangereux, d'où la langue l'interdit.

Une référence rvalue nommée est une lvalue, comme toute autre variable.

La solution consiste à activer manuellement le déplacement:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

On pourrait dire que ce parametern'est plus utilisé après l'initialisation de member. Pourquoi n'y a-t-il pas de règle spéciale à insérer silencieusement std::movecomme pour les valeurs de retour? Probablement parce que ce serait trop lourd pour les implémenteurs du compilateur. Par exemple, que se passe-t-il si le corps du constructeur se trouve dans une autre unité de traduction? En revanche, la règle de valeur de retour doit simplement vérifier les tables de symboles pour déterminer si l'identifiant après le returnmot - clé désigne un objet automatique.

Vous pouvez également transmettre la parametervaleur par. Pour les types à déplacement uniquement comme unique_ptr, il semble qu'il n'y ait pas encore d'idiome établi. Personnellement, je préfère passer par valeur, car cela provoque moins d'encombrement dans l'interface.

Fonctions spéciales des membres

C ++ 98 déclare implicitement trois fonctions membres spéciales à la demande, c'est-à-dire lorsqu'elles sont nécessaires quelque part: le constructeur de copie, l'opérateur d'affectation de copie et le destructeur.

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

Les références Rvalue sont passées par plusieurs versions. Depuis la version 3.0, C ++ 11 déclare à la demande deux fonctions membres spéciales supplémentaires: le constructeur de déplacement et l'opérateur d'affectation de déplacement. Notez que ni VC10 ni VC11 ne sont encore conformes à la version 3.0, vous devrez donc les implémenter vous-même.

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator

Ces deux nouvelles fonctions membres spéciaux ne sont déclarées implicitement que si aucune des fonctions membres spéciales n'est déclarée manuellement. De plus, si vous déclarez votre propre constructeur de déplacement ou opérateur d'affectation de déplacement, ni le constructeur de copie ni l'opérateur d'affectation de copie ne seront déclarés implicitement.

Que signifient ces règles dans la pratique?

Si vous écrivez une classe sans ressources non gérées, il n'est pas nécessaire de déclarer vous-même l'une des cinq fonctions membres spéciales, et vous obtiendrez une copie sémantique correcte et déplacerez la sémantique gratuitement. Sinon, vous devrez implémenter vous-même les fonctions spéciales des membres. Bien sûr, si votre classe ne bénéficie pas de la sémantique des mouvements, il n'est pas nécessaire d'implémenter les opérations de déplacement spéciales.

Notez que l'opérateur d'affectation de copie et l'opérateur d'affectation de déplacement peuvent être fusionnés en un seul opérateur d'affectation unifié, en prenant son argument par valeur:

X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}

De cette façon, le nombre de fonctions membres spéciales à implémenter passe de cinq à quatre. Il y a un compromis à faire entre sécurité d'exception et efficacité ici, mais je ne suis pas un expert en la matière.

Renvoi de références ( précédemment appelées références universelles )

Considérez le modèle de fonction suivant:

template<typename T>
void foo(T&&);

Vous pouvez vous attendre T&&à ne vous lier qu'à rvalues, car à première vue, cela ressemble à une référence rvalue. Comme il s'avère cependant, T&&se lie également aux valeurs l:

foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

Si l'argument est une valeur r de type X, Tse déduit être X, donc T&&moyen X&&. C'est ce à quoi tout le monde s'attendrait. Mais si l'argument est une lvalue de type X, en raison d'une règle spéciale, Ton en déduit être X&, par conséquent T&&signifierait quelque chose comme X& &&. Mais puisque C ++ n'a toujours aucune notion de références aux références, le type X& &&est réduit en X&. Cela peut sembler déroutant et inutile au début, mais l'effondrement des références est essentiel pour une transmission parfaite (qui ne sera pas discutée ici).

T&& n'est pas une référence de valeur, mais une référence de transfert. Il se lie également aux valeurs l, auquel cas Tet T&&sont toutes deux des références lvalue.

Si vous souhaitez contraindre un modèle de fonction à rvalues, vous pouvez combiner SFINAE avec des traits de type:

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);

Mise en œuvre du déménagement

Maintenant que vous comprenez l'effondrement des références, voici comment std::moveest implémenté:

template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

Comme vous pouvez le voir, moveaccepte tout type de paramètre grâce à la référence de transfert T&&et renvoie une référence rvalue. L' std::remove_reference<T>::typeappel de la méta-fonction est nécessaire car sinon, pour les valeurs de type X, le type de retour serait X& &&, ce qui s'effondrerait X&. Étant donné qu'il ts'agit toujours d'une valeur l (rappelez-vous qu'une référence rvalue nommée est une valeur l), mais que nous voulons nous lier tà une référence rvalue, nous devons explicitement convertir tle type de retour correct. L'appel d'une fonction qui renvoie une référence rvalue est lui-même une valeur x. Vous savez maintenant d'où viennent les valeurs x;)

L'appel d'une fonction qui renvoie une référence rvalue, telle que std::move, est une valeur x.

Notez que le retour par référence rvalue est correct dans cet exemple, car tne dénote pas un objet automatique, mais plutôt un objet qui a été transmis par l'appelant.

fredoverflow
la source
24
Il y a une troisième raison pour laquelle la sémantique des mouvements est importante: la sécurité des exceptions. Souvent, lorsqu'une opération de copie peut lancer (car elle doit allouer des ressources et que l'allocation peut échouer), une opération de déplacement peut être non lancée (car elle peut transférer la propriété des ressources existantes au lieu d'en allouer de nouvelles). Il est toujours agréable d'avoir des opérations qui ne peuvent pas échouer, et cela peut être crucial lors de l'écriture de code qui fournit des garanties d'exception.
Brangdon
8
J'étais avec vous jusqu'aux «références universelles», mais c'est trop abstrait pour suivre. La référence s'effondre? Transfert parfait? Êtes-vous en train de dire qu'une référence rvalue devient une référence universelle si le type est modélisé? J'aimerais qu'il y ait un moyen d'expliquer cela pour que je sache si j'ai besoin de le comprendre ou pas! :)
Kylotan
8
S'il vous plaît, écrivez un livre maintenant ... cette réponse m'a donné des raisons de croire que si vous couvriez d'autres coins du C ++ de manière lucide comme celui-ci, des milliers d'autres personnes le comprendraient.
halivingston
12
@halivingston Merci beaucoup pour vos aimables commentaires, je l'apprécie vraiment. Le problème avec l'écriture d'un livre est: c'est beaucoup plus de travail que vous ne pouvez l'imaginer. Si vous voulez approfondir le C ++ 11 et au-delà, je vous suggère d'acheter "Effective Modern C ++" par Scott Meyers.
fredoverflow
77

Les sémantiques de déplacement sont basées sur des références rvalue .
Une rvalue est un objet temporaire, qui va être détruit à la fin de l'expression. Dans le C ++ actuel, les valeurs r ne se lient qu'aux constréférences. C ++ 1x autorise les constréférences non rvalue, orthographiées T&&, qui sont des références à des objets rvalue.
Puisqu'une valeur r va mourir à la fin d'une expression, vous pouvez voler ses données . Au lieu de le copier dans un autre objet, vous y déplacez ses données.

class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor

Dans le code ci - dessus, avec d' anciens compilateurs le résultat de f()est copié en xutilisant Xde » constructeur de copie. Si votre compilateur prend en charge la sémantique de déplacement et Xa un constructeur de déplacement, alors cela est appelé à la place. Puisque son rhsargument est une valeur r , nous savons que ce n'est plus nécessaire et nous pouvons voler sa valeur.
Ainsi, la valeur est déplacée du temporaire sans nom renvoyé de f()à x(tandis que les données de x, initialisées sur un vide X, sont déplacées dans le temporaire, qui sera détruit après l'affectation).

sbi
la source
1
notez que cela devrait être this->swap(std::move(rhs));parce que les références nommées rvalue sont lvalues
wmamrak
C'est un peu faux, selon le commentaire de @ Tacyt: rhsest une lvalue dans le contexte de X::X(X&& rhs). Vous devez appeler std::move(rhs)pour obtenir une valeur r, mais cela rend la réponse théorique.
Asherah
Que déplace la sémantique pour les types sans pointeurs? Déplacer la sémantique fonctionne comme une copie?
Gusev Slava
@Gusev: Je n'ai aucune idée de ce que vous demandez.
sbi
60

Supposons que vous ayez une fonction qui renvoie un objet substantiel:

Matrix multiply(const Matrix &a, const Matrix &b);

Lorsque vous écrivez du code comme celui-ci:

Matrix r = multiply(a, b);

puis un compilateur C ++ ordinaire créera un objet temporaire pour le résultat de multiply(), appellera le constructeur de copie pour initialiser r, puis détruira la valeur de retour temporaire. Les sémantiques de déplacement en C ++ 0x permettent au "constructeur de déplacement" d'être appelé pour s'initialiser ren copiant son contenu, puis de supprimer la valeur temporaire sans avoir à la détruire.

Cela est particulièrement important si (comme peut-être l' Matrixexemple ci-dessus), l'objet copié alloue de la mémoire supplémentaire sur le tas pour stocker sa représentation interne. Un constructeur de copie devrait soit faire une copie complète de la représentation interne, soit utiliser le comptage des références et la sémantique de copie sur écriture en interne. Un constructeur de déplacement laisserait la mémoire de tas seule et copierait simplement le pointeur à l'intérieur de l' Matrixobjet.

Greg Hewgill
la source
2
En quoi les constructeurs de déplacement et les constructeurs de copie sont-ils différents?
dicroce
1
@dicroce: Ils diffèrent par la syntaxe, l'un ressemble à Matrix (const Matrix & src) (constructeur de copie) et l'autre ressemble à Matrix (Matrix && src) (constructeur de déplacement), consultez ma réponse principale pour un meilleur exemple.
snk_kid
3
@dicroce: On fait un objet vide, et on fait une copie. Si les données stockées dans l'objet sont volumineuses, une copie peut coûter cher. Par exemple, std :: vector.
Billy ONeal
1
@ kunj2aan: Cela dépend de votre compilateur, je suppose. Le compilateur peut créer un objet temporaire à l'intérieur de la fonction, puis le déplacer dans la valeur de retour de l'appelant. Ou, il peut être en mesure de construire directement l'objet dans la valeur de retour, sans avoir besoin d'utiliser un constructeur de déplacement.
Greg Hewgill
2
@Jichao: C'est une optimisation appelée RVO, voir cette question pour plus d'informations sur la différence: stackoverflow.com/questions/5031778/…
Greg Hewgill
30

Si vous êtes vraiment intéressé par une bonne explication approfondie de la sémantique des mouvements, je vous recommande fortement de lire le document original à leur sujet, «Une proposition pour ajouter le support de la sémantique des mouvements au langage C ++».

Il est très accessible et facile à lire et il constitue un excellent argument pour les avantages qu'ils offrent. Il existe d'autres articles plus récents et à jour sur la sémantique des mouvements disponibles sur le site Web du WG21 , mais celui-ci est probablement le plus simple car il aborde les choses d'une vue de haut niveau et n'entre pas beaucoup dans les détails du langage.

James McNellis
la source
27

Déplacer la sémantique consiste à transférer des ressources plutôt qu'à les copier lorsque personne n'a plus besoin de la valeur source.

En C ++ 03, les objets sont souvent copiés, uniquement pour être détruits ou affectés avant qu'un code n'utilise à nouveau la valeur. Par exemple, lorsque vous retournez par valeur à partir d'une fonction - sauf si RVO entre en action - la valeur que vous renvoyez est copiée dans le cadre de pile de l'appelant, puis elle sort du domaine et est détruite. Ce n'est qu'un exemple parmi tant d'autres: voir la valeur de passage lorsque l'objet source est temporaire, des algorithmes comme sortcelui-ci ne font que réorganiser les éléments, la réallocation en vectorcas de capacity()dépassement, etc.

Lorsque de telles paires copier / détruire coûtent cher, c'est généralement parce que l'objet possède une ressource lourde. Par exemple, vector<string>peut posséder un bloc de mémoire alloué dynamiquement contenant un tableau d' stringobjets, chacun avec sa propre mémoire dynamique. La copie d'un tel objet est coûteuse: vous devez allouer de la nouvelle mémoire pour chaque bloc alloué dynamiquement dans la source, et copier toutes les valeurs à travers. Ensuite, vous devez libérer toute la mémoire que vous venez de copier. Cependant, déplacer un grand vector<string>signifie simplement copier quelques pointeurs (qui font référence au bloc de mémoire dynamique) vers la destination et les mettre à zéro dans la source.

Dave Abrahams
la source
23

En termes simples (pratiques):

Copier un objet signifie copier ses membres "statiques" et appeler l' newopérateur pour ses objets dynamiques. Droite?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

Cependant, déplacer un objet (je le répète, d'un point de vue pratique) implique seulement de copier les pointeurs des objets dynamiques, et non d'en créer de nouveaux.

Mais n'est-ce pas dangereux? Bien sûr, vous pourriez détruire deux fois un objet dynamique (défaut de segmentation). Donc, pour éviter cela, vous devez "invalider" les pointeurs source pour éviter de les détruire deux fois:

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

D'accord, mais si je déplace un objet, l'objet source devient inutile, non? Bien sûr, mais dans certaines situations, c'est très utile. Le plus évident est lorsque j'appelle une fonction avec un objet anonyme (temporel, objet rvalue, ..., vous pouvez l'appeler avec des noms différents):

void heavyFunction(HeavyType());

Dans cette situation, un objet anonyme est créé, ensuite copié dans le paramètre de fonction, puis supprimé. Donc, ici, il est préférable de déplacer l'objet, car vous n'avez pas besoin de l'objet anonyme et vous pouvez économiser du temps et de la mémoire.

Cela conduit au concept d'une référence "rvalue". Ils existent en C ++ 11 uniquement pour détecter si l'objet reçu est anonyme ou non. Je pense que vous savez déjà qu'une "lvalue" est une entité assignable (la partie gauche de l' =opérateur), donc vous avez besoin d'une référence nommée à un objet pour pouvoir agir comme une lvalue. Une valeur r est exactement le contraire, un objet sans références nommées. Pour cette raison, objet anonyme et rvalue sont synonymes. Donc:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

Dans ce cas, lorsqu'un objet de type Adoit être "copié", le compilateur crée une référence lvalue ou une référence rvalue selon que l'objet passé est nommé ou non. Sinon, votre constructeur de déplacement est appelé et vous savez que l'objet est temporel et vous pouvez déplacer ses objets dynamiques au lieu de les copier, économisant ainsi de l'espace et de la mémoire.

Il est important de se rappeler que les objets "statiques" sont toujours copiés. Il n'y a aucun moyen de "déplacer" un objet statique (objet dans la pile et non sur le tas). Ainsi, la distinction "déplacer" / "copier" lorsqu'un objet n'a pas de membres dynamiques (directement ou indirectement) n'est pas pertinente.

Si votre objet est complexe et que le destructeur a d'autres effets secondaires, comme appeler la fonction d'une bibliothèque, appeler d'autres fonctions globales ou quoi que ce soit, il vaut peut-être mieux signaler un mouvement avec un drapeau:

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

Ainsi, votre code est plus court (vous n'avez pas besoin de faire une nullptraffectation pour chaque membre dynamique) et plus général.

Autre question typique: quelle est la différence entre A&&et const A&&? Bien sûr, dans le premier cas, vous pouvez modifier l'objet et dans le second non, mais, sens pratique? Dans le second cas, vous ne pouvez pas le modifier, vous n'avez donc aucun moyen d'invalider l'objet (sauf avec un indicateur mutable ou quelque chose comme ça), et il n'y a aucune différence pratique pour un constructeur de copie.

Et quelle est la transmission parfaite ? Il est important de savoir qu'une "référence rvalue" est une référence à un objet nommé dans la "portée de l'appelant". Mais dans la portée réelle, une référence rvalue est un nom à un objet, elle agit donc comme un objet nommé. Si vous transmettez une référence rvalue à une autre fonction, vous passez un objet nommé, donc, l'objet n'est pas reçu comme un objet temporel.

void some_function(A&& a)
{
   other_function(a);
}

L'objet aserait copié dans le paramètre réel de other_function. Si vous voulez que l'objet acontinue à être traité comme un objet temporaire, vous devez utiliser la std::movefonction:

other_function(std::move(a));

Avec cette ligne, std::movesera aconverti en une valeur r et other_functionrecevra l'objet en tant qu'objet sans nom. Bien sûr, s'il other_functionn'y a pas de surcharge spécifique pour travailler avec des objets sans nom, cette distinction n'est pas importante.

Est-ce une transmission parfaite? Non, mais nous sommes très proches. Un transfert parfait n'est utile que pour travailler avec des modèles, dans le but de dire: si j'ai besoin de passer un objet à une autre fonction, j'ai besoin que si je reçois un objet nommé, l'objet soit passé en tant qu'objet nommé, et dans le cas contraire, Je veux le passer comme un objet sans nom:

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

C'est la signature d'une fonction prototypique qui utilise une transmission parfaite, implémentée en C ++ 11 au moyen de std::forward. Cette fonction exploite certaines règles d'instanciation de modèle:

 `A& && == A&`
 `A&& && == A&&`

Donc, si Test une référence lvalue à A( T = A &), aaussi ( A & && => A &). Si Test une référence de valeur à A, aégalement (A && && => A &&). Dans les deux cas, aest un objet nommé dans la portée réelle, mais Tcontient les informations de son "type de référence" du point de vue de la portée de l'appelant. Ces informations ( T) sont transmises en tant que paramètre de modèle à forwardet «a» est déplacé ou non selon le type de T.

Peregring-lk
la source
20

C'est comme copier la sémantique, mais au lieu d'avoir à dupliquer toutes les données, vous obtenez de voler les données de l'objet à "déplacer".

Terry Mahaffey
la source
13

Vous savez ce que signifie une sémantique de copie, n'est-ce pas? cela signifie que vous avez des types qui sont copiables, pour les types définis par l'utilisateur, vous définissez cela soit en écrivant explicitement un constructeur de copie et un opérateur d'affectation, soit le compilateur les génère implicitement. Cela fera une copie.

La sémantique de déplacement est fondamentalement un type défini par l'utilisateur avec un constructeur qui prend une référence de valeur r (nouveau type de référence utilisant && (oui deux esperluettes)) qui n'est pas const, cela s'appelle un constructeur de déplacement, il en va de même pour l'opérateur d'affectation. Alors, que fait un constructeur de déplacement, au lieu de copier la mémoire de son argument source, il «déplace» la mémoire de la source vers la destination.

Quand voudriez-vous faire ça? bien std :: vector est un exemple, disons que vous avez créé un std :: vector temporaire et que vous le retournez à partir d'une fonction, dites:

std::vector<foo> get_foos();

Vous allez avoir des frais généraux du constructeur de copie lorsque la fonction revient, si (et il le fera en C ++ 0x) std :: vector a un constructeur de déplacement au lieu de le copier, il peut simplement définir ses pointeurs et «déplacer» alloué dynamiquement mémoire à la nouvelle instance. C'est un peu comme la sémantique du transfert de propriété avec std :: auto_ptr.

snk_kid
la source
1
Je ne pense pas que ce soit un excellent exemple, car dans ces exemples de valeur de retour de fonction, l'optimisation de la valeur de retour élimine probablement déjà l'opération de copie.
Zan Lynx
7

Pour illustrer le besoin de déplacer la sémantique , considérons cet exemple sans déplacer la sémantique:

Voici une fonction qui prend un objet de type Tet renvoie un objet du même type T:

T f(T o) { return o; }
  //^^^ new object constructed

La fonction ci-dessus utilise l' appel par valeur, ce qui signifie que lorsque cette fonction est appelée, un objet doit être construit pour être utilisé par la fonction.
Étant donné que la fonction renvoie également par valeur , un autre nouvel objet est construit pour la valeur de retour:

T b = f(a);
  //^ new object constructed

Deux nouveaux objets ont été construits, dont l'un est un objet temporaire qui n'est utilisé que pour la durée de la fonction.

Lorsque le nouvel objet est créé à partir de la valeur de retour, le constructeur de copie est appelé pour copier le contenu de l'objet temporaire vers le nouvel objet b. Une fois la fonction terminée, l'objet temporaire utilisé dans la fonction sort du cadre et est détruit.


Voyons maintenant ce que fait un constructeur de copie .

Il doit d'abord initialiser l'objet, puis copier toutes les données pertinentes de l'ancien objet vers le nouveau.
Selon la classe, c'est peut-être un conteneur avec beaucoup de données, alors cela pourrait représenter beaucoup de temps et d' utilisation de la mémoire

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

Avec la sémantique de déplacement, il est désormais possible de rendre la plupart de ce travail moins désagréable en déplaçant simplement les données plutôt qu'en les copiant.

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

Le déplacement des données implique de réassocier les données au nouvel objet. Et aucune copie n'a lieu du tout.

Ceci est accompli avec une rvalueréférence.
Une rvalueréférence fonctionne à peu près comme une lvalueréférence avec une différence importante:
une référence rvalue peut être déplacée et une lvalue ne peut pas.

De cppreference.com :

Pour rendre possible une garantie d'exception forte, les constructeurs de déplacement définis par l'utilisateur ne doivent pas lever d'exceptions. En fait, les conteneurs standard s'appuient généralement sur std :: move_if_noexcept pour choisir entre déplacer et copier lorsque les éléments de conteneur doivent être déplacés. Si les constructeurs copy et move sont fournis, la résolution de surcharge sélectionne le constructeur move si l'argument est une rvalue (soit une prvalue telle qu'un temporaire sans nom ou une xvalue telle que le résultat de std :: move), et sélectionne le constructeur de copie si l'argument est une lvalue (objet nommé ou fonction / opérateur renvoyant une référence lvalue). Si seul le constructeur de copie est fourni, toutes les catégories d'arguments le sélectionnent (tant qu'il prend une référence à const, car les valeurs r peuvent se lier aux références const), ce qui rend la copie de secours pour le déplacement, lorsque le déplacement n'est pas disponible. Dans de nombreuses situations, les constructeurs de déplacement sont optimisés même s'ils produisent des effets secondaires observables, voir élision de copie. Un constructeur est appelé «constructeur de déplacement» lorsqu'il prend une référence rvalue comme paramètre. Il n'est pas obligatoire de déplacer quoi que ce soit, la classe n'est pas obligée d'avoir une ressource à déplacer et un «constructeur de déplacement» peut ne pas être en mesure de déplacer une ressource comme dans le cas autorisé (mais peut-être pas sensible) où le paramètre est un const rvalue référence (const T&&).

Andreas DM
la source
7

J'écris ceci pour m'assurer de bien le comprendre.

La sémantique de déplacement a été créée pour éviter la copie inutile de gros objets. Bjarne Stroustrup dans son livre "The C ++ Programming Language" utilise deux exemples où la copie inutile se produit par défaut: un, l'échange de deux gros objets, et deux, le retour d'un grand objet d'une méthode.

L'échange de deux grands objets implique généralement la copie du premier objet dans un objet temporaire, la copie du deuxième objet dans le premier objet et la copie de l'objet temporaire dans le deuxième objet. Pour un type intégré, c'est très rapide, mais pour les gros objets, ces trois copies peuvent prendre beaucoup de temps. Une "affectation de déplacement" permet au programmeur de remplacer le comportement de copie par défaut et de remplacer les références aux objets, ce qui signifie qu'il n'y a aucune copie du tout et que l'opération de permutation est beaucoup plus rapide. L'affectation de déplacement peut être invoquée en appelant la méthode std :: move ().

Le retour d'un objet d'une méthode par défaut implique de faire une copie de l'objet local et de ses données associées dans un emplacement accessible à l'appelant (car l'objet local n'est pas accessible à l'appelant et disparaît lorsque la méthode se termine). Lorsqu'un type intégré est renvoyé, cette opération est très rapide, mais si un objet volumineux est renvoyé, cela peut prendre du temps. Le constructeur de déplacement permet au programmeur de remplacer ce comportement par défaut et de "réutiliser" les données de tas associées à l'objet local en pointant l'objet renvoyé à l'appelant pour stocker les données associées à l'objet local. Ainsi, aucune copie n'est requise.

Dans les langages qui ne permettent pas la création d'objets locaux (c'est-à-dire les objets de la pile), ces types de problèmes ne se produisent pas car tous les objets sont alloués sur le tas et sont toujours accessibles par référence.

Chris B
la source
"Une" affectation de déplacement "permet au programmeur de remplacer le comportement de copie par défaut et de remplacer les références aux objets, ce qui signifie qu'il n'y a pas de copie du tout et que l'opération de permutation est beaucoup plus rapide." - ces affirmations sont ambiguës et trompeuses. Pour échanger deux objets xet y, vous ne pouvez pas simplement "échanger des références aux objets" ; il se peut que les objets contiennent des pointeurs qui référencent d'autres données, et ces pointeurs peuvent être échangés, mais les opérateurs de déplacement ne sont pas tenus de permuter quoi que ce soit. Ils peuvent effacer les données de l'objet déplacé, plutôt que de conserver les données de destination qui s'y trouvent.
Tony Delroy
Vous pouvez écrire swap()sans déplacer la sémantique. "L'affectation de déplacement peut être invoquée en appelant la méthode std :: move ()." - il est parfois nécessaire d'utiliser std::move()- bien que cela ne déplace rien - laisse simplement le compilateur savoir que l'argument est mobile, parfois std::forward<>()(avec des références de transfert), et d'autres fois le compilateur sait qu'une valeur peut être déplacée.
Tony Delroy
-2

Voici une réponse du livre "The C ++ Programming Language" de Bjarne Stroustrup. Si vous ne voulez pas voir la vidéo, vous pouvez voir le texte ci-dessous:

Considérez cet extrait. Le retour d'un opérateur + implique de copier le résultat hors de la variable locale reset dans un endroit où l'appelant peut y accéder.

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_siz e_mismatch{};
    Vector res(a.size());
        for (int i=0; i!=a.size(); ++i)
            res[i]=a[i]+b[i];
    return res;
}

Nous ne voulions pas vraiment de copie; nous voulions juste obtenir le résultat d'une fonction. Nous devons donc déplacer un vecteur plutôt que de le copier. Nous pouvons définir le constructeur de déplacement comme suit:

class Vector {
    // ...
    Vector(const Vector& a); // copy constructor
    Vector& operator=(const Vector& a); // copy assignment
    Vector(Vector&& a); // move constructor
    Vector& operator=(Vector&& a); // move assignment
};

Vector::Vector(Vector&& a)
    :elem{a.elem}, // "grab the elements" from a
    sz{a.sz}
{
    a.elem = nullptr; // now a has no elements
    a.sz = 0;
}

Le && signifie "rvalue reference" et est une référence à laquelle nous pouvons lier une rvalue. "rvalue" 'est destiné à compléter "lvalue" qui signifie à peu près "quelque chose qui peut apparaître sur le côté gauche d'une affectation". Ainsi, une valeur r signifie à peu près "une valeur à laquelle vous ne pouvez pas affecter", comme un entier renvoyé par un appel de fonction, et la resvariable locale dans l'opérateur + () pour les vecteurs.

Maintenant, la déclaration return res;ne sera pas copiée!

Rob Pei
la source