Question Que sont les sémantiques de mouvement?


Je viens d'écouter la radio Software Engineering interview de podcast avec Scott Meyers En ce qui concerne C ++ 0x. La plupart des nouvelles fonctionnalités ont du sens pour moi, et je suis vraiment excité à propos de C ++ 0x maintenant, à l'exception d'un. Je ne reçois toujours pas déplacer la sémantique... Qu'est-ce qu'ils sont exactement?


1374
2018-06-23 22:46


origine


Réponses:


Je trouve qu'il est plus facile de comprendre la sémantique du mouvement 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 = strlen(p) + 1;
        data = new char[size];
        memcpy(data, p, size);
    }

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

    ~string()
    {
        delete[] data;
    }

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

Le constructeur de copie définit ce que signifie copier les objets chaîne. Le paramètre const string& that 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

Maintenant vient la compréhension clé de la sémantique du mouvement. Notez que seulement dans la première ligne où nous copions x cette copie profonde est-elle vraiment nécessaire, parce que nous pourrions vouloir inspecter x plus tard et serait très surpris si x avait changé en quelque sorte. Avez-vous remarqué comment je viens de dire x trois 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 dans les lignes 2 et 3 ne sont pas lvalues, mais rvalues, parce que les objets string sous-jacents n'ont pas de noms, de sorte que le client n'a aucun moyen de les inspecter à un moment ultérieur. Les rvalues ​​désignent les 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 valeur de rvalue). Ceci est important car lors de l'initialisation de b et c, nous pourrions faire tout 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 choses, 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. A l'intérieur de ce constructeur, nous pouvons faire tout ce que nous voulons avec la source, aussi longtemps que nous le laissons dans certains é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 avons simplement copié le pointeur, puis défini le pointeur d'origine sur null. 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 ne peut détecter que la source a été modifiée. Puisque nous ne faisons pas vraiment de copie ici, nous appelons ce constructeur un "constructeur de mouvement". 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 du mouvement! Continuons en implémentant l'opérateur d'affectation. Si vous n'êtes pas familier avec le copier et échanger l'idiome, apprenez-le et revenez, parce que c'est un idiome génial de C ++ lié à la sécurité d'exception.

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

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

Notez que nous passons le paramètre that  en valeur, alors that doit être initialisé comme tout autre objet chaîne. Exactement comment that va être initialisé? Dans les temps anciens de C ++ 98, la réponse aurait été "par le constructeur de la 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 un lvalue ou un rvalue.

Donc si vous dites a = b, la constructeur de copie va initialiser that (parce que l'expression b est une lvalue), et l'opérateur d'affectation échange le contenu avec une copie profonde nouvellement 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, et ensuite débarrassez-vous de la copie en laissant la portée. Rien de nouveau ici.

Mais si tu dis a = x + y, la déplacer le constructeur va initialiser that (parce que l'expression x + y est un rvalue), donc il n'y a pas de copie profonde impliquée, seulement un mouvement efficace. that est encore un objet indépendant de l'argument, mais sa construction était triviale, puisque les données de tas n'ont pas dû être copiées, juste déplacées. Il n'était pas nécessaire de le copier parce que x + y est un rvalue, et encore une fois, il est bon de passer des objets string dénotés par rvalues.

Pour récapituler, le constructeur de copie fait une copie profonde, parce que la source doit rester intacte. D'autre part, le constructeur de déplacement peut simplement copier le pointeur, puis définir le pointeur de la source sur null. Il est correct de "neutraliser" l'objet source de cette manière, car le client n'a aucun moyen d'inspecter à nouveau l'objet.

J'espère que cet exemple aura le point principal. Il y a beaucoup plus à faire pour référencer les références et déplacer la sémantique que j'ai délibérément omise pour rester simple. Si vous voulez plus de détails s'il vous plaît voir ma réponse supplémentaire.


2034
2018-06-24 12:40



Ma première réponse a été une introduction extrêmement simplifiée à la sémantique des déplacements, et de nombreux détails ont été laissés de côté afin de rester simples. Cependant, il y a beaucoup plus à faire pour déplacer la sémantique, et j'ai pensé 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 n'a pas semblé juste de la remplacer par un texte complètement différent. Je pense que ça sert encore bien comme une première introduction. Mais si vous voulez creuser plus profond, lisez :)

Stephan T. Lavavej a pris le temps de fournir des commentaires précieux. 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 à travers ses objets membres), la sémantique de déplacement n'offrira 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. La mise en œuvre de types «déplacement uniquement» sécuritaires; c'est-à-dire, les types pour lesquels la copie n'a pas de sens, mais le fait de le faire. Les exemples incluent les verrous, les handles de fichiers et les pointeurs intelligents avec une sémantique de propriété unique. Note: Cette réponse discute std::auto_ptr, un modèle de bibliothèque standard C ++ 98 obsolète, qui a été remplacé par std::unique_ptr en C ++ 11. Les programmeurs intermédiaires C ++ sont probablement au moins quelque peu familiers avec std::auto_ptr, et à cause 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 des déplacements dans C ++ 11. YMMV.

Qu'est-ce qu'un mouvement?

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

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

La chose inhabituelle à propos de auto_ptr 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 comment l'initialisation de b avec a Est-ce que ne pas copier le triangle, mais transfère à la place la propriété du triangle de a à b. Nous disons aussi "a est déménagé dans  b"ou" le triangle est déplacé de a  à  bCela peut sembler confus, car le triangle lui-même reste toujours au même endroit en mémoire.

Déplacer un objet signifie transférer la propriété de certaines ressources qu'il gère vers un autre objet.

Le constructeur de copie de auto_ptr ressemble probablement à quelque chose comme ça (un 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

La chose dangereuse à propos de auto_ptr est-ce que ce qui ressemble syntaxiquement à une copie est en fait un mouvement. Essayer d'appeler une fonction membre sur un mouvement-de auto_ptr invoquera un comportement indéfini, vous devez donc faire très attention de ne pas utiliser un auto_ptr après avoir été déplacé de:

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

Mais auto_ptr n'est pas toujours dangereux. Fonctions d'usine sont un cas d'utilisation parfaitement bien 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 schéma syntaxique:

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

Et pourtant, l'un d'entre eux invoque un comportement indéfini, alors que l'autre ne l'est pas. Alors, quelle est la différence entre les expressions a et make_triangle()? Ne sont-ils pas tous les deux du même type? En effet, ils le sont, mais ils ont différents catégories de valeur.

Catégories de valeur

De toute évidence, il doit y avoir une différence profonde entre l'expression a ce qui dénote un auto_ptr variable, et l'expression make_triangle() ce qui dénote l'appel d'une fonction qui renvoie un auto_ptr par la valeur, créant ainsi un frais temporaire auto_ptr objet chaque fois qu'il est appelé. a est un exemple d'un lvalue, tandis que make_triangle() est un exemple d'un Rvalue.

Passer de lvalues ​​telles que a est dangereux, car nous pourrions plus tard essayer d'appeler une fonction membre via a, invoquant un comportement indéfini. D'un autre côté, passer de valeurs telles que make_triangle() est parfaitement sûr, car après que le constructeur de copie a fait son travail, nous ne pouvons plus utiliser le temporaire. Il n'y a pas d'expression qui dénote ledit temporaire; si nous écrivons simplement make_triangle()encore une fois, nous obtenons un différent temporaire. En fait, le mouvement de temporaire 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 l et r avoir une origine historique dans le côté gauche et le côté droit d'une affectation. Ce n'est plus vrai en C ++, car il y a des lvalues ​​qui ne peuvent apparaître sur le côté gauche d'une assignation (comme des tableaux ou des types définis par l'utilisateur sans opérateur d'assignation), et il y a des valeurs qui peuvent (toutes les valeurs des classes avec un opérateur d'affectation).

Une valeur 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.

Rvalue références

Nous comprenons maintenant que le déplacement à partir de lvalues ​​est potentiellement dangereux, mais le passage des valeurs est inoffensif. Si C ++ avait un support de langage pour distinguer les arguments de lvalue des arguments de rvalue, nous pourrions soit interdire totalement de passer de lvalues, soit au moins faire un déplacement à partir de lvalues explicite sur le site d'appel, de sorte que nous ne déménageons plus par accident.

La réponse de C ++ 11 à ce problème est références rvalue. Une référence rvalue est un nouveau type de référence qui ne lie que des valeurs de rvalues, et la syntaxe est X&&. La bonne vieille référence X& est maintenant connu comme référence lvalue. (Notez que X&& est ne pas une référence à une référence; il n'y a pas une telle chose en C ++.)

Si nous jetons const dans le mélange, nous avons déjà quatre types de références différentes. Quels types d'expressions de type X peuvent-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&&. Être limité à lire des valeurs n'est pas très utile.

Une référence rvalue X&& est un nouveau type de référence qui ne lie que les valeurs.

Conversions implicites

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

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

some_function("hello world");

Dans l'exemple ci-dessus, "hello world" est une lvalue de type const char[12]. Comme il y a une conversion implicite de const char[12] par const char* à std::string, un temporaire de type std::string est créé, et r est lié à ce temporaire. C'est l'un des cas où la distinction entre les valeurs (expressions) et les temporaires (objets) est un peu floue.

Déplacer les constructeurs

Un exemple utile d'une fonction avec un X&& paramètre est le déplacer le constructeur  X::X(X&& source). Son but est de transférer la propriété de la ressource gérée de la source dans l'objet courant.

En C ++ 11, std::auto_ptr<T> a été remplacé par std::unique_ptr<T> qui tire parti des références rvalue. Je vais développer et discuter d'une version simplifiée de unique_ptr. Premièrement, nous encapsulons un pointeur brut et surchargons les opérateurs -> et *, donc 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;
    }

Maintenant vient 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 mouvement fait exactement ce que le auto_ptr constructeur de copie a fait, mais il ne peut être fourni avec des valeurs:

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 a est une lvalue, mais le paramètre unique_ptr&& source ne peut être lié qu'à des valeurs. C'est exactement ce que nous voulions. Les mouvements dangereux ne devraient jamais être implicites. La troisième ligne compile très bien, car make_triangle() est une valeur. Le constructeur de déménagement transférera la propriété de la temporaire à 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 mouvement. 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 mouvement duplique la logique du constructeur du destructeur et du moteur de déplacement. Connaissez-vous l'idiome copy-and-swap? Il peut également être appliqué pour déplacer la sémantique comme l'idiome move-and-swap:

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

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

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

Passer des valeurs

Parfois, nous voulons passer de lvalues. C'est-à-dire que parfois nous voulons que le compilateur traite une lvalue comme s'il s'agissait d'un rvalue, de sorte qu'il puisse invoquer le constructeur de déplacement, même s'il pourrait être potentiellement dangereux. A cet effet, C ++ 11 propose un modèle de fonction de bibliothèque standard appelé std::move dans l'en-tête <utility>. Ce nom est un peu malheureux, car std::move jette simplement une lvalue à une valeur; Cela fait ne pas déplacer quelque chose par lui-même. C'est simplement permet en mouvement. Peut-être qu'il aurait dû être nommé std::cast_to_rvalue ou std::enable_move, mais nous sommes bloqués avec le nom maintenant.

Voici comment vous vous déplacez explicitement d'une lvalue:

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, a ne possède plus de triangle. C'est bon, parce que par explicitement l'écriture std::move(a), nous avons fait nos intentions claires: "Cher constructeur, fais ce que tu veux avec a pour initialiser c; Je me fiche de a plus. Sentez-vous libre d'avoir votre chemin avec a"

std::move(some_lvalue) jette une lvalue à une valeur, permettant ainsi un mouvement ultérieur.

Xvalues

Notez que même si std::move(a) est une valeur, son évaluation ne ne pas créer un 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, même si ce n'est pas une valeur au sens traditionnel, est appelé xvalue (valeur d'exportation). Les valeurs traditionnelles ont été renommées prvalues (Rvalues ​​pures).

Les valeurs prvalues ​​et xvalues ​​sont des valeurs rvalues. Les valeurs X et les valeurs sont toutes deux Glvalues (Valeurs 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 renommage et au regroupement.

Les valeurs C ++ 98 sont appelées prvalues ​​dans C ++ 11. Remplacer mentalement toutes les occurrences de "rvalue" dans les paragraphes précédents avec "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 dans la direction opposée. 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 le return instruction en tant qu'argument du constructeur de mouvement:

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

Peut-être étonnamment, les objets automatiques (variables locales qui ne sont pas déclarées comme static) peut également être implicitement sorti des fonctions:

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

Comment le constructeur du déménagement accepte-t-il la lvalue? result comme argument? La portée de result est sur le point de se terminer, et il sera détruit pendant le déroulement de la pile. Personne ne pourrait éventuellement se plaindre que result avait changé d'une manière ou d'une autre; lorsque le flux de contrôle est de retour à l'appelant, result n'existe plus! Pour cette raison, C ++ 11 dispose d'une règle spéciale qui permet de renvoyer des objets automatiques depuis des fonctions sans devoir écrire std::move. En fait, vous devriez jamais utilisation std::move pour retirer les objets automatiques des fonctions, car cela inhibe l'optimisation de la valeur de retour nommée (NRVO).

Ne jamais utiliser std::move pour déplacer les objets automatiques hors des fonctions.

Notez que dans les deux fonctions d'usine, le type de retour est une valeur et non une référence de valeur. Les références Rvalue sont toujours des références, et comme toujours, vous ne devriez jamais renvoyer une référence à un objet automatique; l'appelant se retrouverait avec une référence dangling si vous avez trompé le compilateur en acceptant 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 les objets automatiques par référence rvalue. Le déplacement est exclusivement effectué par le constructeur de déplacement, et non par std::move, et non en liant simplement une valeur à une référence de référence.

Passer aux 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 que parameter est une lvalue. 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 à un rvalue"; Cela fait ne pas signifie que la référence elle-même est une valeur! Effectivement, parameter est juste une variable ordinaire avec un nom. Vous pouvez utiliser parameter aussi souvent que vous le voulez dans le corps du constructeur, et cela dénote toujours le même objet. S'en déplacer implicitement serait dangereux, d'où le langage 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
    {}
};

Vous pourriez soutenir que parameter n'est plus utilisé après l'initialisation de member. Pourquoi n'y a-t-il pas de règle spéciale pour insérer silencieusement std::move tout comme avec les valeurs de retour? Probablement parce que ce serait trop de fardeau sur les implémenteurs du compilateur. Par exemple, que se passe-t-il si le corps du constructeur est 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'identificateur après le return mot-clé désigne un objet automatique.

Vous pouvez également passer parameter en valeur. Pour les types à déplacement uniquement unique_ptr, il semble qu'il n'y a pas encore d'idiome établi. Personnellement, je préfère passer en 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 de Rvalue ont traversé plusieurs versions. Depuis la version 3.0, C ++ 11 déclare deux fonctions membres spéciales supplémentaires à la demande: le constructeur de déplacement et l'opérateur d'affectation de mouvement. 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éciales ne sont déclarées implicitement que si aucune des fonctions membres spéciales n'est déclarée manuellement. De même, 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 managé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 sémantique de copie correcte et une sémantique de déplacement gratuite. Sinon, vous devrez vous-même implémenter les fonctions membres spéciales. Bien sûr, si votre classe ne bénéficie pas de la sémantique de déplacement, 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 mouvement peuvent être fusionnés en un seul opérateur d'affectation unifié, en prenant son argument en 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 entre l'exception-sécurité et l'efficacité ici, mais je ne suis pas un expert sur cette question.

Références de renvoi (précédemment connu comme Références universelles)

Considérez le modèle de fonction suivant:

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

Vous pourriez vous attendre T&& de ne lier que les valeurs, car à première vue, cela ressemble à une référence de référence. Comme il se trouve cependant, T&& se lie également aux valeurs:

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 de type X, T est déduit pour être X, Par conséquent T&& veux dire 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, T est déduit pour être X&, Par conséquent T&& signifierait quelque chose comme X& &&. Mais puisque C ++ n'a toujours pas de notion de références à des références, le type X& && est s'est effondré dans X&. Cela peut sembler déroutant et inutile au début, mais l'effondrement des références est essentiel pour transfert parfait (qui ne sera pas discuté ici).

T && n'est pas une référence de référence, mais une référence de transfert. Il lie également aux valeurs, auquel cas T et T&& sont les deux références de lvalue.

Si vous voulez contraindre un modèle de fonction à des valeurs, vous pouvez combiner SFINAE avec des caractères 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 mouvement

Maintenant que vous comprenez l'effondrement des références, voici comment std::move est 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, move accepte tout type de paramètre grâce à la référence de transfert T&&, et il renvoie une référence rvalue. le std::remove_reference<T>::type L'appel de méta-fonction est nécessaire car sinon, pour les lvalues ​​de type X, le type de retour serait X& &&, qui s'effondrerait en X&. Depuis t est toujours une lvalue (rappelez-vous qu'une référence rvalue nommée est une lvalue), mais nous voulons lier t à une référence rvalue, nous devons explicitement jeter t au type de retour correct. L'appel d'une fonction qui renvoie une référence rvalue est lui-même une valeur x. Maintenant, vous savez 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 renvoyer par référence rvalue est très bien dans cet exemple, car t ne désigne pas un objet automatique, mais un objet qui a été transmis par l'appelant.


891
2017-07-18 11:24



La sémantique de déplacement est basée sur références rvalue.
Un rvalue est un objet temporaire, qui va être détruit à la fin de l'expression. Dans le C ++ actuel, les valeurs ne se lient qu'à const les références. C ++ 1x permettra non-const références rvalue, épeautre T&&, qui sont des références à un objet rvalue.
Puisqu'une valeur va mourir à la fin d'une expression, vous pouvez voler ses données. Au lieu de copier dans un autre objet, vous bouge toi ses données en elle.

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 les anciens compilateurs, le résultat de f() est copié dans x en utilisant Xle constructeur de la copie. Si votre compilateur prend en charge la sémantique de déplacement et X a un constructeur de mouvement, alors cela s'appelle à la place. Depuis ses rhs l'argument est un Rvalue, nous savons que ce n'est plus nécessaire et nous pouvons voler sa valeur.
Donc la valeur est déplacé à partir de la temporaire sans nom retourné de f() à x (alors que les données de x, initialisé à un vide X, est déplacé dans le temporaire, qui sera détruit après l'affectation).


67
2018-06-23 23:12



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 ceci:

Matrix r = multiply(a, b);

alors un compilateur C ++ ordinaire créera un objet temporaire pour le résultat de multiply(), appelle le constructeur de la copie pour l'initialiser r, puis détruisez la valeur de retour temporaire. Déplacer la sémantique en C ++ 0x permet d'appeler le "constructeur de déplacement" pour initialiser ren copiant son contenu, puis supprimez la valeur temporaire sans avoir à le détruire.

Ceci est particulièrement important si (comme peut-être le Matrix exemple ci-dessus), l'objet en cours de copie 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 de références et la sémantique de copie-écriture. Un constructeur de déplacement laisserait la mémoire de tas seule et copierait simplement le pointeur dans le Matrix objet.


46
2018-06-23 22:53



Si vous êtes vraiment intéressé par une bonne explication en profondeur de la sémantique du mouvement, je vous recommande fortement de lire le document original sur eux, "Une proposition pour ajouter le support de sémantique de déplacement au langage C ++." 

Il est très accessible et facile à lire, ce qui en fait un excellent exemple pour les avantages qu'ils offrent. Il existe d'autres documents plus récents et à jour sur la sémantique des mouvements disponibles sur le site web du GT21, mais celui-ci est probablement le plus simple car il aborde les choses à partir d'une vue de haut niveau et n'aborde pas très bien les détails de la langue.


27
2018-06-23 23:32



Déplacer la sémantique est à propos transférer des ressources plutôt que de les copier quand personne n'a plus besoin de la valeur source.

En C ++ 03, les objets sont souvent copiés, seulement pour être détruits ou assignés - avant que le code n'utilise à nouveau la valeur. Par exemple, lorsque vous revenez en valeur à partir d'une fonction (sauf si RVO intervient), la valeur que vous renvoyez est copiée dans le cadre de la pile de l'appelant, puis elle est hors de portée et est détruite. C'est juste un exemple parmi d'autres: voir la valeur de passage lorsque l'objet source est temporaire, des algorithmes comme sort que juste réorganiser les éléments, réallocation dans vector quand il est capacity() est dépassé, etc.

Lorsque de telles paires copier / détruire sont chères, 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 de string objets, 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. alors vous devez libérer toute la mémoire que vous venez de copier. cependant, en mouvement un grand vector<string> signifie simplement copier quelques pointeurs (qui se réfèrent au bloc de mémoire dynamique) à la destination et les mettre à zéro dans la source.


21
2018-04-08 19:47



En termes simples (pratiques):

Copier un objet signifie copier ses membres "statiques" et appeler le new opé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, pour bouge toi un objet (je le répète, d'un point de vue pratique) implique seulement de copier les pointeurs d'objets dynamiques, et non d'en créer de nouveaux.

Mais, n'est-ce pas dangereux? Bien sûr, vous pouvez détruire deux fois un objet dynamique (faute de segmentation). Donc, pour éviter cela, vous devez "invalider" les pointeurs sources 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. 
};

Ok, 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. La plus évidente est quand j'appelle une fonction avec un objet anonyme (temporel, objet rvalue, ..., vous pouvez l'appeler avec des noms différents):

void heavyFunction(HeavyType());

Dans ce cas, un objet anonyme est créé, copié ensuite dans le paramètre de la 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 gagner du temps et de la mémoire.

Cela conduit à la notion de 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 la = opérateur), vous avez donc besoin d'une référence nommée à un objet pour pouvoir agir en tant que lvalue. Une valeur est exactement le contraire, un objet sans références nommées. Pour cette raison, l'objet anonyme et la valeur sont des synonymes. Alors:

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, quand un objet de type A devrait être "copié", le compilateur crée une référence lvalue ou une référence rvalue selon si l'objet passé est nommé ou non. Si ce n'est pas le cas, votre constructeur de mouvement est appelé et vous savez que l'objet est temporel et vous pouvez déplacer ses objets dynamiques au lieu de les copier, en économisant 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 pas sur le tas). Ainsi, la distinction "déplacer" / "copier" quand 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 */ }
};

Donc, votre code est plus court (vous n'avez pas besoin de faire un nullptr affectation pour chaque membre dynamique) et plus générale.

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 pas, mais, sens pratique? Dans le second cas, vous ne pouvez pas le modifier, donc vous n'avez aucun moyen d'invalider l'objet (sauf avec un drapeau mutable ou quelque chose comme ça), et il n'y a pas de différence pratique avec un constructeur de copie.

Et qu'est ce qui est transfert parfait? 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, donc, il agit comme un objet nommé. Si vous passez 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'object a serait copié sur le paramètre réel de other_function. Si vous voulez l'objet a continue d'être traité comme un objet temporaire, vous devez utiliser le std::move fonction:

other_function(std::move(a));

Avec cette ligne, std::move jettera a à une valeur et other_function recevra l'objet sous la forme d'un objet sans nom. Bien sûr si other_functionn'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. Le transfert parfait n'est utile que pour travailler avec des templates, dans le but de dire: si je dois passer un objet à une autre fonction, j'en ai besoin si je reçois un objet nommé, l'objet est passé comme un objet nommé, 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 un transfert parfait, implémenté 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 T est une référence lvalue à A (T = A &), a aussi (UNE& && => A &). Si T est une référence rvalue à A, a aussi (A && && => A &&). Dans les deux cas, a est un objet nommé dans la portée réelle, mais T contient les informations de son "type de référence" du point de vue de la portée de l'appelant. Cette information (T) est passé en paramètre de modèle forward et 'a' est déplacé ou non selon le type de T.


19
2017-08-18 15:57



C'est comme la sémantique de la copie, mais au lieu d'avoir à dupliquer toutes les données que vous obtenez pour voler les données de l'objet en cours de "déplacement".


17
2018-06-23 22:56



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

Move sémantique est essentiellement un type défini par l'utilisateur avec un constructeur qui prend une référence r (nouveau type de référence utilisant && (yes two esperands)) qui est non-const, c'est ce qu'on appelle un constructeur de déplacement. 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 cela? bien std :: vector est un exemple, disons que vous avez créé un vecteur std :: temporaire et que vous le renvoyez d'une fonction dites:

std::vector<foo> get_foos();

Vous allez avoir un surcoût du constructeur de la copie quand la fonction retourne, si (et il le fera en C ++ 0x) std :: vector a un constructeur de déplacement au lieu de le copier peut juste définir ses pointeurs et 'déplacer' dynamiquement alloué mémoire à la nouvelle instance. C'est un peu comme la sémantique du transfert de propriété avec std :: auto_ptr.


13
2018-06-23 22:58



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

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

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

La fonction ci-dessus utilise appel par valeur ce qui signifie que lorsque cette fonction est appelée un objet doit être construit être utilisé par la fonction.
Parce que la fonction aussi retourne en valeur, un autre nouvel objet est construit pour la valeur de retour:

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

Deux de nouveaux objets ont été construits, dont un est un objet temporaire utilisé uniquement pour la durée de la fonction.

Lorsque le nouvel objet est créé à partir de la valeur de retour, le constructeur de la copie est appelé copie le contenu de l'objet temporaire au nouvel objet b. Une fois la fonction terminée, l'objet temporaire utilisé dans la fonction est hors de portée et est détruit.


Maintenant, considérons ce que constructeur de copie Est-ce que.

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 temps et 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 déplacer la sémantique il est maintenant possible de rendre la plupart de ce travail moins désagréable simplement en mouvement les données plutôt que de les copier.

// 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 un rvalue référence.
Un rvalue la référence fonctionne à peu près comme un lvalue référence avec une différence importante:
un La référence rvalue peut être déplacée Et un lvalue ne peux pas.

De cppreference.com:

Pour que la garantie d'exception forte soit possible, les constructeurs de mouvement définis par l'utilisateur ne doivent pas lancer d'exceptions. En fait, les conteneurs standards s'appuient généralement sur std :: move_if_noexcept pour choisir entre move et copy lorsque les éléments conteneur doivent être déplacés.   Si à la fois les constructeurs de copie et de déplacement sont fournis, la résolution de surcharge sélectionne le constructeur de déplacement si l'argument est un rvalue (soit un prvalue tel qu'un temporaire sans nom ou une valeur x comme le résultat de std :: move) et sélectionne le constructeur de copie l'argument est un lvalue (objet nommé ou une 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 (à condition qu'il prenne une référence à const, puisque les valeurs peuvent se lier aux références const), ce qui rend le repli pour le déplacement, quand le déplacement est indisponible.   Dans de nombreuses situations, les constructeurs de mouvement sont optimisés même s'ils produisent des effets secondaires observables, voir Elision de copie.   Un constructeur est appelé un 'constructeur de déplacement' lorsqu'il prend une référence de valeur en tant que paramètre. Il n'est pas obligé 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). Valeur de référence constante (const T &&).


6
2018-02-25 00:00



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 d'objets volumineux. Bjarne Stroustrup dans son livre "Le langage de programmation C ++" utilise deux exemples où la copie inutile se produit par défaut: un, l'échange de deux grands objets, et deux, le retour d'un grand objet d'une méthode.

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

Renvoyer 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 (parce que l'objet local n'est pas accessible à l'appelant et disparaît à la fin de la méthode). Lorsqu'un type intégré est renvoyé, cette opération est très rapide, mais si un objet volumineux est renvoyé, cela peut prendre beaucoup de 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 langues qui ne permettent pas la création d'objets locaux (c'est-à-dire les objets sur 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.


3
2017-11-18 23:12