Qu’est-ce que la sémantique de mouvement?

Je viens de terminer l’écoute du 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 en fait enthousiaste à propos de C ++ 0x, à l’exception d’une. Je n’ai toujours pas de sémantique de mouvement … Qu’est-ce qu’ils sont exactement?

Je trouve plus facile de comprendre la sémantique du mouvement avec un exemple de code. Commençons par une classe de chaîne très simple qui ne contient qu’un pointeur sur un bloc de mémoire alloué au tas:

#include  #include  class ssortingng { char* data; public: ssortingng(const char* p) { size_t size = strlen(p) + 1; data = new char[size]; memcpy(data, p, size); } 

Puisque nous avons choisi de gérer nous-mêmes la mémoire, nous devons suivre la règle des 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 l’instant:

  ~ssortingng() { delete[] data; } ssortingng(const ssortingng& 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 des objects de chaîne. Le paramètre const ssortingng& that se lie à toutes les expressions de type ssortingng qui vous permettent de faire des copies dans les exemples suivants:

 ssortingng a(x); // Line 1 ssortingng b(x + y); // Line 2 ssortingng c(some_function_returning_a_ssortingng()); // Line 3 

Vient maintenant la clé de la sémantique du mouvement. Notez que ce n’est que dans la première ligne où nous copions x cette copie profonde est vraiment nécessaire, car nous pourrions vouloir inspecter x plus tard et serions très surpris si x avait changé. Avez-vous remarqué comment je viens de dire x trois fois (quatre fois si vous incluez cette phrase) et signifie exactement le même object à 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 objects de chaîne sous-jacents n’ont pas de nom, de sorte que le client n’a aucun moyen de les inspecter ultérieurement. Les rvalues ​​désignent les objects temporaires qui sont détruits au prochain point-virgule (pour être plus précis: à la fin de la pleine expression qui contient lexiquement la valeur). Ceci est important car lors de l’initialisation de b et c , nous pouvions 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, nous permet de détecter les arguments rvalue via la surcharge de la fonction. Il suffit 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 le source, à condition de le laisser dans un état valide:

  ssortingng(ssortingng&& that) // ssortingng&& is an rvalue reference to a ssortingng { 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. En effet, nous avons “volé” les données qui appartenaient à l’origine à la chaîne source. Encore une fois, la principale idée est que le client ne pouvait en aucun cas détecter que la source avait été modifiée. Comme 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 object à un autre au lieu de les copier.

Félicitations, vous comprenez maintenant les bases de la sémantique de mouvement! 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 langage C ++ génial lié à la sécurité des exceptions.

  ssortingng& operator=(ssortingng 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 passons le paramètre par valeur , de sorte that doit être initialisé comme n’importe quel autre object de chaîne. Comment that va-t-il être initialisé? Dans les jours 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 une lvalue ou une rvalue.

Donc, si vous dites a = b , le constructeur de la copie l’initialisera (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, puis débarrassez-vous de la copie en quittant la scope. Rien de nouveau ici.

Mais si vous dites a = x + y , le constructeur de mouvement initialisera that (parce que l’expression x + y est une valeur), il n’y a donc pas de copie en profondeur, seulement un déplacement efficace. that toujours un object indépendant de l’argument, mais sa construction était sortingviale, puisque les données du tas n’avaient pas à être copiées, juste déplacées. Il n’était pas nécessaire de le copier car x + y est une valeur, et encore une fois, il est acceptable de déplacer des objects chaîne dénotés par des valeurs.

Pour résumer, le constructeur de la copie effectue une copie complète, car la source doit restr 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 possible de “annuler” l’object source de cette manière, car le client ne peut plus inspecter l’object.

J’espère que cet exemple a été le sharepoint départ. Il y a beaucoup plus à faire pour réfuter les références et déplacer la sémantique que j’ai intentionnellement omise pour restr simple. Si vous voulez plus de détails, veuillez consulter ma réponse supplémentaire .

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é volontairement ignorés pour restr simple. Cependant, il y a beaucoup plus de choses à déplacer la sémantique, et j’ai pensé qu’il était temps de répondre à une seconde réponse pour combler les lacunes. La première réponse est déjà assez ancienne et il ne semblait pas juste de le remplacer par un texte complètement différent. Je pense que ça sert encore bien comme première introduction. Mais si vous voulez aller plus loin, 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 object, sous certaines conditions, de s’approprier les ressources externes d’un autre object. Ceci est important de deux manières:

  1. Transformer des copies chères en mouvements bon marché. Voir ma première réponse pour un exemple. Notez que si un object ne gère pas au moins une ressource externe (directement ou indirectement via ses objects membres), la sémantique de déplacement n’offre aucun avantage par rapport à la sémantique de copie. Dans ce cas, copier un object et déplacer un object 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. Mettre en œuvre des types de “déplacement uniquement” sûrs; c’est-à-dire des types pour lesquels la copie n’a pas de sens, mais c’est le cas pour le déplacement. Les exemples incluent des verrous, des descripteurs de fichiers et des pointeurs intelligents avec une sémantique de propriété unique. Remarque: Cette réponse traite de std::auto_ptr , un modèle de bibliothèque standard C ++ 98 obsolète, remplacé par std::unique_ptr en C ++ 11. Les programmeurs C ++ intermédiaires sont probablement au moins quelque peu familiers avec std::auto_ptr , et en raison de la “sémantique des mouvements”, cela semble être un bon sharepoint 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 offre un pointeur intelligent avec une sémantique de propriété unique appelée std::auto_ptr . Si vous n’êtes pas familier avec auto_ptr , son objective est de garantir qu’un object alloué dynamicment est toujours libéré, même en présence d’exceptions:

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

Ce qui est inhabituel avec auto_ptr est son comportement de "copie":

 auto_ptr a(new Triangle); +---------------+ | sortingangle data | +---------------+ ^ | | | +-----|---+ | +-|-+ | a | p | | | | | +---+ | +---------+ auto_ptr b(a); +---------------+ | sortingangle data | +---------------+ ^ | +----------------------+ | +---------+ +-----|---+ | +---+ | | +-|-+ | a | p | | | b | p | | | | | +---+ | | +---+ | +---------+ +---------+ 

Notez que l'initialisation de b avec a ne copie pas le sortingangle, mais transfère la propriété du sortingangle de a à b . Nous disons aussi " a est déplacé dans b " ou "le sortingangle est déplacé de a à b ". Cela peut paraître déroutant, car le sortingangle lui-même rest toujours au même endroit en mémoire.

Déplacer un object signifie transférer la propriété d'une ressource gérée à un autre object.

Le constructeur de copie de auto_ptr ressemble probablement à 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 sans danger

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

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

Mais auto_ptr n'est pas toujours dangereux. Les fonctions d'usine sont un cas d'utilisation parfait pour auto_ptr :

 auto_ptr make_sortingangle() { return auto_ptr(new Triangle); } auto_ptr c(make_sortingangle()); // move temporary into c double area = make_sortingangle()->area(); // perfectly safe 

Notez comment les deux exemples suivent le même schéma syntaxique:

 auto_ptr 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. Alors, quelle est la différence entre les expressions a et make_sortingangle() ? Ne sont-ils pas tous les deux du même type? En effet, ils le sont, mais ils ont des catégories de valeur différentes.

Catégories de valeur

De toute évidence, il doit y avoir une différence profonde entre l'expression a qui désigne une variable auto_ptr et l'expression make_sortingangle() qui désigne l'appel d'une fonction qui renvoie un auto_ptr par valeur, créant ainsi un nouvel object auto_ptr temporaire chaque fois qu'il est appelé . a est un exemple de lvalue , alors que make_sortingangle() est un exemple de rvalue .

Passer de lvalues ​​telles que a est dangereux, car nous pourrions plus tard essayer d'appeler une fonction membre via a comportement indéfini appelant. D'un autre côté, passer de valeurs telles que make_sortingangle() est parfaitement sûr, car une fois que le constructeur de la 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_sortingangle() , nous obtenons un autre temporaire. En fait, le temporaire transféré est déjà passé à la ligne suivante:

 auto_ptr c(make_sortingangle()); ^ the moved-from temporary dies right here 

Notez que les lettres l et r ont une origine historique dans la partie gauche et la partie droite d'une affectation. Ce n'est plus vrai en C ++, car il existe des valeurs qui ne peuvent pas apparaître à gauche d'une affectation (comme des tableaux ou des types définis par l'utilisateur sans opérateur d'affectation), et il existe des valeurs possibles (toutes les valeurs des types de classe) avec un opérateur d'affectation).

Une valeur de type de classe est une expression dont l'évaluation crée un object temporaire. Dans des circonstances normales, aucune autre expression dans la même scope ne désigne le même object temporaire.

Références Rvalue

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

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

Si nous jetons const dans le mélange, nous avons déjà quatre types de références différents. A 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é à la lecture de rvalues ​​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 Rvalue ont traversé 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 Y différent, à condition qu'il y ait une conversion implicite de Y en X Dans ce cas, un temporaire de type X est créé et la référence rvalue est liée à ce temporaire:

 void some_function(std::ssortingng&& 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] en const char* en std::ssortingng , un temporaire de type std::ssortingng est créé, et r est lié à ce temporaire. C'est l'un des cas où la distinction entre les rvalues ​​(expressions) et les temporaires (objects) est un peu floue.

Déplacer des constructeurs

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

En C ++ 11, std::auto_ptr a été remplacé par std::unique_ptr qui tire parti des références rvalue. Je vais développer et discuter d'une version simplifiée de unique_ptr . Tout d'abord, nous encapsulons un pointeur brut et surchargons les opérateurs -> et * , de sorte que notre classe ressemble à un pointeur:

 template class unique_ptr { T* ptr; public: T* operator->() const { return ptr; } T& operator*() const { return *ptr; } 

Le constructeur prend possession de l'object 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 déplacement fait exactement ce que faisait le constructeur de la copie auto_ptr , mais il ne peut être fourni qu'avec des valeurs:

 unique_ptr a(new Triangle); unique_ptr b(a); // error unique_ptr c(make_sortingangle()); // okay 

La deuxième ligne ne parvient pas à comstackr, 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 comstack très bien, car make_sortingangle() est une valeur. Le constructeur de déménagement transférera la propriété du 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'object en cours.

Déplacer des opérateurs d'affectation

La dernière pièce manquante est l'opérateur d'atsortingbution 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 déplacement duplique la logique du constructeur du destructeur et du constructeur de déplacement. Êtes-vous familier avec l'idiome de copie et d'échange? Il peut également être appliqué pour déplacer la sémantique comme 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 cette source 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, car le constructeur de déplacement lui-même a un paramètre de référence rvalue. Lorsque le stream de contrôle atteint l'accolade de fermeture de l' operator= , la source sort de la scope, 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'object en cours, libérant l'ancienne ressource. L'idiome move-and-swap simplifie l'implémentation.

Passer des lvalues

Parfois, nous voulons passer des lvalues. C'est-à-dire que parfois nous voulons que le compilateur traite une lvalue comme s'il s'agissait d'une rvalue, donc il peut invoquer le constructeur de déplacement, même s'il peut être potentiellement dangereux. Pour cela, C ++ 11 propose un modèle de fonction de bibliothèque standard appelé std::move dans l'en-tête . Ce nom est un peu malheureux, car std::move simplement une lvalue en rvalue; il ne bouge rien par lui-même. Cela permet simplement de bouger. Peut-être aurait-il dû s'appeler std::cast_to_rvalue ou std::enable_move , mais nous sums maintenant bloqués avec le nom.

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

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

Notez qu'après la troisième ligne, a ne possède plus de sortingangle. Ce n'est pas grave, car en écrivant explicitement std::move(a) , nous avons clarifié nos intentions: "Cher constructeur, faites ce que vous voulez avec a afin d'initialiser c ; je m'en fiche plus. N'hésitez pas à votre chemin avec a . "

std::move(some_lvalue) lance une valeur sur une valeur, permettant ainsi un déplacement ultérieur.

Xvalues

Notez que même si std::move(a) est une valeur, son évaluation ne crée pas d'object temporaire. Cette énigme a obligé le comité à introduire une troisième catégorie de valeur. Quelque chose qui peut être lié à une référence de valeur, même s'il ne s'agit pas d'une valeur au sens traditionnel, s'appelle une valeur xvalue (valeur eXpirante). Les rvalues ​​traditionnelles ont été rebaptisées prvalues (Pure rvalues).

Les valeurs et les valeurs sont toutes deux des valeurs. Les xvalues ​​et les lvalues ​​sont toutes deux des glvalues (lvalues ​​généralisées). Les relations sont plus faciles à saisir avec un diagramme:

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

Notez que seules les valeurs xval sont vraiment nouvelles. le rest est dû au changement de nom et au regroupement.

Les valeurs C ++ 98 sont connues sous le nom de valeurs dans C ++ 11. Remplacer mentalement toutes les occurrences de "rvalue" dans les paragraphes précédents par "prvalue".

Sortir des fonctions

Jusqu'à présent, nous avons vu un mouvement dans les variables locales et dans les parameters de fonction. Mais bouger est également possible dans la direction opposée. Si une fonction retourne par valeur, un object sur le site d'appel (probablement une variable locale ou un object temporaire, mais pourrait être n'importe quel type d'object) est initialisé avec l'expression après l'instruction return en tant qu'argument du constructeur de déplacement:

 unique_ptr make_sortingangle() { return unique_ptr(new Triangle); } \-----------------------------/ | | temporary is moved into c | v unique_ptr c(make_sortingangle()); 

Peut-être de manière surprenante, les objects automatiques (variables locales qui ne sont pas déclarées static ) peuvent également être implicitement déplacés hors des fonctions:

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

Comment se fait-il que le constructeur de mouvements accepte le result lvalue comme argument? La scope du result est sur le sharepoint se terminer, et il sera détruit lors du déroulement de la stack. Personne ne pourrait plus se plaindre par la result que ce result avait changé en quelque sorte; lorsque le stream de contrôle est de retour chez l'appelant, le result n'existe plus! Pour cette raison, C ++ 11 a une règle spéciale qui permet de renvoyer des objects automatiques à partir de fonctions sans avoir à écrire std::move . En fait, vous ne devez jamais utiliser std::move pour déplacer des objects automatiques hors des fonctions, car cela empêche l’optimisation de la valeur de retour nommée (NRVO).

Ne jamais utiliser std::move pour déplacer des objects 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 devez jamais renvoyer une référence à un object automatique. l'appelant se retrouverait avec une référence pendante si vous aviez amené le compilateur à accepter votre code, comme ceci:

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

Ne jamais retourner les objects automatiques par référence à la valeur. Le déplacement est exclusivement effectué par le constructeur de mouvement, pas par std::move , et pas simplement en liant une valeur à une référence de valeur.

Emménager dans les membres

Tôt ou tard, vous allez écrire du code comme celui-ci:

 class Foo { unique_ptr member; public: Foo(unique_ptr&& parameter) : member(parameter) // error {} }; 

Fondamentalement, le compilateur se plaindra que ce 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 liée à une valeur"; cela ne signifie pas que la référence elle-même est une valeur! En effet, le parameter est juste une variable ordinaire avec un nom. Vous pouvez utiliser parameter aussi souvent que vous le souhaitez dans le corps du constructeur, et il désigne toujours le même object. S'en éloigner implicitement serait dangereux, donc le langage l’interdit.

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

La solution consiste à activer manuellement le déplacement:

 class Foo { unique_ptr member; public: Foo(unique_ptr&& parameter) : member(std::move(parameter)) // note the std::move {} }; 

Vous pourriez soutenir que ce parameter n'est plus utilisé après l'initialisation du member . Pourquoi n'y a-t-il pas de règle spéciale pour insérer silencieusement std::move comme avec les valeurs de retour? Probablement parce que ce serait trop lourd pour les développeurs du compilateur. Par exemple, que faire si le corps du constructeur était 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 situé après le mot-clé de return indique ou non un object automatique.

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

Fonctions membres spéciales

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 la copie, l'opérateur d'assignation 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 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 déplacement. Notez que ni VC10 ni VC11 ne sont 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 votre propre opérateur d'assignation de déplacement, ni le constructeur de copie ni l'opérateur d'assignation 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 les cinq fonctions membres spécifiques et vous obtiendrez une sémantique de copie correcte et vous déplacerez la sémantique gratuitement. Sinon, vous devrez implémenter vous-même les fonctions spéciales du membre. Bien entendu, 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'assignation de copie et l'opérateur d'assignation 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 manière, le nombre de fonctions membres spéciales à implémenter passe de cinq à quatre. Il y a un compromis entre la sécurité des exceptions et l'efficacité ici, mais je ne suis pas un expert dans ce domaine.

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

Considérons le modèle de fonction suivant:

 template void foo(T&&); 

Vous pourriez vous attendre à ce que T&& ne se lie qu'à des valeurs, car à première vue, cela ressemble à une référence de valeur. Il s'avère que T&& se lie également aux lvalues:

 foo(make_sortingangle()); // T is unique_ptr, T&& is unique_ptr&& unique_ptr a(new Triangle); foo(a); // T is unique_ptr&, T&& is unique_ptr& 

Si l'argument est une valeur de type X , on en déduit que T est X , donc T&& signifie X&& . C'est ce à quoi on pourrait s'attendre. Mais si l'argument est une lvalue de type X , à cause d'une règle spéciale, on en déduit que T est X& , donc T&& signifierait quelque chose comme X& && . Mais C ++ n’ayant toujours aucune notion de référence aux références, le type X& && est réduit dans X& . Cela peut sembler déroutant et inutile au début, mais la réduction de la référence est essentielle pour un acheminement parfait (ce qui ne sera pas discuté ici).

T && n'est pas une référence de valeur, mais une référence de transmission. Il se lie également à lvalues, auquel cas T et T&& sont tous deux des références lvalue.

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

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

Mise en place du déménagement

Maintenant que vous comprenez la réduction de la référence, voici comment std::move est implémenté:

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

Comme vous pouvez le voir, move accepte tout type de paramètre grâce à la référence de transfert T&& et renvoie une référence de valeur. L'appel de méta-fonction std::remove_reference::type est nécessaire car sinon, pour les lvalues ​​de type X , le type de retour serait X& && , ce qui réduirait en X& . Puisque t est toujours une lvalue (rappelez-vous qu'une référence nommée rvalue est une lvalue), mais que nous voulons lier une référence rvalue, nous devons explicitement convertir t en type de retour correct. L'appel d'une fonction qui retourne une référence rvalue est lui-même une xvalue. Maintenant, vous savez d'où viennent les valeurs x;)

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

Notez que le renvoi par référence rvalue est correct dans cet exemple, car t ne dénote pas un object automatique, mais plutôt un object transmis par l'appelant.

La sémantique de déplacement est basée sur des références de valeur .
Une rvalue est un object temporaire qui va être détruit à la fin de l’expression. Dans le C ++ actuel, les valeurs ne sont liées qu’aux références const . C ++ 1x autorisera les références non constantes, orthographiées T&& , qui sont des références à des objects rvalue.
Comme une valeur va mourir à la fin d’une expression, vous pouvez voler ses données . Au lieu de le copier dans un autre object, 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 les anciens compilateurs, le résultat de f() est copié dans x utilisant le constructeur de copie de X Si votre compilateur prend en charge la sémantique de déplacement et que X a un constructeur de mouvements, cela est appelé à la place. Comme son argument est une valeur , nous soaps que ce n’est plus nécessaire et que nous pouvons en voler la valeur.
Ainsi, la valeur est déplacée du temporaire non nommé renvoyé de f() vers x (alors que les données de x , initialisées à un X vide, sont déplacées dans le temporaire, qui sera détruit après l’affectation).

Supposons que vous ayez une fonction qui retourne un object substantiel:

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

Lorsque vous écrivez du code comme ceci:

 Masortingx r = multiply(a, b); 

alors un compilateur C ++ ordinaire créera un object temporaire pour le résultat de multiply() , appelle le constructeur de copie pour initialiser r , puis détruit la valeur de retour temporaire. La sémantique de déplacement en C ++ 0x permet d’appeler le “constructeur de déplacement” pour initialiser r en copiant son contenu, puis de supprimer la valeur temporaire sans la détruire.

Ceci est particulièrement important si (comme peut-être l’exemple de Masortingx ci-dessus), l’object 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 de référence et la sémantique de copie sur écriture. Un constructeur de déplacer laisserait la mémoire de tas seule et copierait simplement le pointeur à l’intérieur de l’object Masortingx .

If you are really interestd in a good, in-depth explanation of move semantics, I’d highly recommend reading the original paper on them, “A Proposal to Add Move Semantics Support to the C++ Language.”

It’s very accessible and easy to read and it makes an excellent case for the benefits that they offer. There are other more recent and up to date papers about move semantics available on the WG21 website , but this one is probably the most straightforward since it approaches things from a top-level view and doesn’t get very much into the gritty language details.

Move semantics is about transferring resources rather than copying them when nobody needs the source value anymore.

In C++03, objects are often copied, only to be destroyed or assigned-over before any code uses the value again. For example, when you return by value from a function—unless RVO kicks in—the value you’re returning is copied to the caller’s stack frame, and then it goes out of scope and is destroyed. This is just one of many examples: see pass-by-value when the source object is a temporary, algorithms like sort that just rearrange items, reallocation in vector when its capacity() is exceeded, etc.

When such copy/destroy pairs are expensive, it’s typically because the object owns some heavyweight resource. For example, vector may own a dynamically-allocated memory block containing an array of ssortingng objects, each with its own dynamic memory. Copying such an object is costly: you have to allocate new memory for each dynamically-allocated blocks in the source, and copy all the values across. Then you need deallocate all that memory you just copied. However, moving a large vector means just copying a few pointers (that refer to the dynamic memory block) to the destination and zeroing them out in the source.

In easy (practical) terms:

Copying an object means copying its “static” members and calling the new operator for its dynamic objects. Droite?

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

However, to move an object (I repeat, in a practical point of view) implies only to copy the pointers of dynamic objects, and not to create new ones.

But, is that not dangerous? Of course, you could destruct a dynamic object twice (segmentation fault). So, to avoid that, you should “invalidate” the source pointers to avoid destructing them twice:

 class A { int i, *p; public: // Movement of an object inside a copy constructor. A(const A& a) : i(ai), p(ap) { ap = nullptr; // pointer invalidated. } ~A() { delete p; } // Deleting NULL, 0 or nullptr (address 0x0) is safe. }; 

Ok, but if I move an object, the source object becomes useless, no? Of course, but in certain situations that’s very useful. The most evident one is when I call a function with an anonymous object (temporal, rvalue object, …, you can call it with different names):

 void heavyFunction(HeavyType()); 

In that situation, an anonymous object is created, next copied to the function parameter, and afterwards deleted. So, here it is better to move the object, because you don’t need the anonymous object and you can save time and memory.

This leads to the concept of an “rvalue” reference. They exist in C++11 only to detect if the received object is anonymous or not. I think you do already know that an “lvalue” is an assignable entity (the left part of the = operator), so you need a named reference to an object to be capable to act as an lvalue. A rvalue is exactly the opposite, an object with no named references. Because of that, anonymous object and rvalue are synonyms. Alors:

 class A { int i, *p; public: // Copy A(const A& a) : i(ai), p(new int(*ap)) {} // Movement (&& means "rvalue reference to") A(A&& a) : i(ai), p(ap) { ap = nullptr; } ~A() { delete p; } }; 

In this case, when an object of type A should be “copied”, the comstackr creates a lvalue reference or a rvalue reference according to if the passed object is named or not. When not, your move-constructor is called and you know the object is temporal and you can move its dynamic objects instead of copying them, saving space and memory.

It is important to remember that “static” objects are always copied. There’s no ways to “move” a static object (object in stack and not on heap). So, the distinction “move”/ “copy” when an object has no dynamic members (directly or indirectly) is irrelevant.

If your object is complex and the destructor has other secondary effects, like calling to a library’s function, calling to other global functions or whatever it is, perhaps is better to signal a movement with a flag:

 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 */ } }; 

So, your code is shorter (you don’t need to do a nullptr assignment for each dynamic member) and more general.

Other typical question: what is the difference between A&& and const A&& ? Of course, in the first case, you can modify the object and in the second not, but, practical meaning? In the second case, you can’t modify it, so you have no ways to invalidate the object (except with a mutable flag or something like that), and there is no practical difference to a copy constructor.

And what is perfect forwarding ? It is important to know that a “rvalue reference” is a reference to a named object in the “caller’s scope”. But in the actual scope, a rvalue reference is a name to an object, so, it acts as a named object. If you pass an rvalue reference to another function, you are passing a named object, so, the object isn’t received like a temporal object.

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

The object a would be copied to the actual parameter of other_function . If you want the object a continues being treated as a temporary object, you should use the std::move function:

 other_function(std::move(a)); 

With this line, std::move will cast a to an rvalue and other_function will receive the object as a unnamed object. Of course, if other_function has not specific overloading to work with unnamed objects, this distinction is not important.

Is that perfect forwarding? Not, but we are very close. Perfect forwarding is only useful to work with templates, with the purpose to say: if I need to pass an object to another function, I need that if I receive a named object, the object is passed as a named object, and when not, I want to pass it like a unnamed object:

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

That’s the signature of a prototypical function that uses perfect forwarding, implemented in C++11 by means of std::forward . This function exploits some rules of template instantiation:

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

So, if T is a lvalue reference to A ( T = A&), a also ( A& && => A&). If T is a rvalue reference to A , a also (A&& && => A&&). In both cases, a is a named object in the actual scope, but T contains the information of its “reference type” from the caller scope’s point of view. This information ( T ) is passed as template parameter to forward and ‘a’ is moved or not according to the type of T .

It’s like copy semantics, but instead of having to duplicate all of the data you get to steal the data from the object being “moved” from.

You know what a copy semantics means right? it means you have types which are copyable, for user-defined types you define this either buy explicitly writing a copy constructor & assignment operator or the comstackr generates them implicitly. This will do a copy.

Move semantics is basically a user-defined type with constructor that takes an r-value reference (new type of reference using && (yes two ampersands)) which is non-const, this is called a move constructor, same goes for assignment operator. So what does a move constructor do, well instead of copying memory from it’s source argument it ‘moves’ memory from the source to the destination.

When would you want to do that? well std::vector is an example, say you created a temporary std::vector and you return it from a function say:

 std::vector get_foos(); 

You’re going to have overhead from the copy constructor when the function returns, if (and it will in C++0x) std::vector has a move constructor instead of copying it can just set it’s pointers and ‘move’ dynamically allocated memory to the new instance. It’s kind of like transfer-of-ownership semantics with std::auto_ptr.

To illustrate the need for move semantics , let’s consider this example without move semantics:

Here’s a function that takes an object of type T and returns an object of the same type T :

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

The above function uses call by value which means that when this function is called an object must be constructed to be used by the function.
Because the function also returns by value , another new object is constructed for the return value:

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

Two new objects have been constructed, one of which is a temporary object that’s only used for the duration of the function.

When the new object is created from the return value, the copy constructor is called to copy the contents of the temporary object to the new object b. After the function completes, the temporary object used in the function goes out of scope and is destroyed.


Now, let’s consider what a copy constructor does.

It must first initialize the object, then copy all the relevant data from the old object to the new one.
Depending on the class, maybe its a container with very much data, then that could represent much time and memory usage

 // 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); } 

With move semantics it’s now possible to make most of this work less unpleasant by simply moving the data rather than copying.

 // 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); } 

Moving the data involves re-associating the data with the new object. And no copy takes place at all.

This is accomplished with an rvalue reference.
An rvalue reference works pretty much like an lvalue reference with one important difference:
an rvalue reference can be moved and an lvalue cannot.

From cppreference.com :

To make strong exception guarantee possible, user-defined move constructors should not throw exceptions. In fact, standard containers typically rely on std::move_if_noexcept to choose between move and copy when container elements need to be relocated. If both copy and move constructors are provided, overload resolution selects the move constructor if the argument is an rvalue (either a prvalue such as a nameless temporary or an xvalue such as the result of std::move), and selects the copy constructor if the argument is an lvalue (named object or a function/operator returning lvalue reference). If only the copy constructor is provided, all argument categories select it (as long as it takes a reference to const, since rvalues can bind to const references), which makes copying the fallback for moving, when moving is unavailable. In many situations, move constructors are optimized out even if they would produce observable side-effects, see copy elision. A constructor is called a ‘move constructor’ when it takes an rvalue reference as a parameter. It is not obligated to move anything, the class is not required to have a resource to be moved and a ‘move constructor’ may not be able to move a resource as in the allowable (but maybe not sensible) case where the parameter is a const rvalue reference (const T&&).

I’m writing this to make sure I understand it properly.

Move semantics were created to avoid the unnecessary copying of large objects. Bjarne Stroustrup in his book “The C++ Programming Language” uses two examples where unnecessary copying occurs by default: one, the swapping of two large objects, and two, the returning of a large object from a method.

Swapping two large objects usually involves copying the first object to a temporary object, copying the second object to the first object, and copying the temporary object to the second object. For a built-in type, this is very fast, but for large objects these three copies could take a large amount of time. A “move assignment” allows the programmer to override the default copy behavior and instead swap references to the objects, which means that there is no copying at all and the swap operation is much faster. The move assignment can be invoked by calling the std::move() method.

Returning an object from a method by default involves making a copy of the local object and its associated data in a location which is accessible to the caller (because the local object is not accessible to the caller and disappears when the method finishes). When a built-in type is being returned, this operation is very fast, but if a large object is being returned, this could take a long time. The move constructor allows the programmer to override this default behavior and instead “reuse” the heap data associated with the local object by pointing the object being returned to the caller to heap data associated with the local object. Thus no copying is required.

In languages which do not allow the creation of local objects (that is, objects on the stack) these types of problems do not occur as all objects are allocated on the heap and are always accessed by reference.