Je viens de finir d'écouter l' interview de podcast radio de Software Engineering avec Scott Meyers concernant C ++ 0x . La plupart des nouvelles fonctionnalités avaient du sens pour moi, et je suis vraiment enthousiasmé par C ++ 0x maintenant, à l'exception d'une. Je n'ai toujours pas de sémantique de mouvement ... Qu'est-ce que c'est exactement?
c++
c++-faq
c++11
move-semantics
dicroce
la source
la source
Réponses:
Je trouve qu'il est plus facile de comprendre la sémantique des mouvements avec un exemple de code. Commençons par une classe de chaînes très simple qui ne contient qu'un pointeur vers un bloc de mémoire alloué par tas:
Puisque nous avons choisi de gérer la mémoire nous-mêmes, nous devons suivre la règle des trois . Je vais différer l'écriture de l'opérateur d'affectation et n'implémenter que le destructeur et le constructeur de copie pour l'instant:
Le constructeur de copie définit ce que signifie copier des objets chaîne. Le paramètre
const string& that
se lie à toutes les expressions de type chaîne qui vous permet de faire des copies dans les exemples suivants:Voici maintenant la clé de la sémantique des mouvements. Notez que ce n'est que dans la première ligne où nous copions que
x
cette copie profonde est vraiment nécessaire, car nous pourrions vouloir inspecterx
plus tard et serions très surpris si ellex
avait changé d'une manière ou d'une autre. Avez-vous remarqué comment je viens de direx
trois fois (quatre fois si vous incluez cette phrase) et signifiait exactement le même objet à chaque fois? Nous appelons des expressions telles quex
"lvalues".Les arguments des lignes 2 et 3 ne sont pas des lvalues, mais des rvalues, car les objets de chaîne sous-jacents n'ont pas de nom, le client n'a donc aucun moyen de les inspecter à nouveau ultérieurement. rvalues désigne des objets temporaires qui sont détruits au point-virgule suivant (pour être plus précis: à la fin de l'expression complète qui contient lexicalement la rvalue). Ceci est important car lors de l'initialisation de
b
etc
, nous pouvions faire ce que nous voulions avec la chaîne source, et le client ne pouvait pas faire la différence !C ++ 0x introduit un nouveau mécanisme appelé "référence rvalue" qui, entre autres, nous permet de détecter les arguments rvalue via une surcharge de fonction. Tout ce que nous avons à faire est d'écrire un constructeur avec un paramètre de référence rvalue. À l'intérieur de ce constructeur, nous pouvons faire tout ce que nous voulons avec la source, tant que nous le laissons dans un état valide:
Qu'avons-nous fait ici? Au lieu de copier en profondeur les données du tas, nous venons de copier le pointeur, puis de définir le pointeur d'origine sur null (pour empêcher «supprimer []» du destructeur de l'objet source de libérer nos «données juste volées»). En effet, nous avons «volé» les données qui appartenaient à l'origine à la chaîne source. Encore une fois, l'idée clé est qu'en aucun cas le client n'a pu détecter que la source avait été modifiée. Puisque nous ne faisons pas vraiment de copie ici, nous appelons ce constructeur un "constructeur de déplacement". Son travail consiste à déplacer des ressources d'un objet à un autre au lieu de les copier.
Félicitations, vous comprenez maintenant les bases de la sémantique des mouvements! Continuons en implémentant l'opérateur d'affectation. Si vous n'êtes pas familier avec l' idiome de copie et d'échange , apprenez-le et revenez, car c'est un idiome C ++ génial lié à la sécurité des exceptions.
Hein, c'est ça? "Où est la référence rvalue?" vous pourriez demander. "Nous n'en avons pas besoin ici!" est ma réponse :)
Notez que nous transmettons le paramètre
that
par valeur , ilthat
doit donc être initialisé comme tout autre objet chaîne. Commentthat
va- t-on exactement être initialisé? Dans les temps anciens de C ++ 98 , la réponse aurait été "par le constructeur de copie". En C ++ 0x, le compilateur choisit entre le constructeur de copie et le constructeur de déplacement selon que l'argument de l'opérateur d'affectation est une lvalue ou une rvalue.Donc, si vous dites
a = b
, le constructeur de copie s'initialisethat
(car l'expressionb
est une valeur l), et l'opérateur d'affectation échange le contenu avec une copie complète fraîchement créée. C'est la définition même de l'idiome de copie et d'échange - faites une copie, échangez le contenu avec la copie, puis débarrassez-vous de la copie en quittant la portée. Rien de nouveau ici.Mais si vous dites
a = x + y
, le constructeur de mouvement s'initialisethat
(car l'expressionx + y
est une valeur r), donc il n'y a pas de copie profonde impliquée, seulement un déplacement efficace.that
est toujours un objet indépendant de l'argument, mais sa construction était triviale, car les données de tas n'avaient pas à être copiées, juste déplacées. Il n'était pas nécessaire de le copier carx + y
c'est une rvalue, et encore une fois, il est correct de se déplacer à partir d'objets chaîne dénotés par rvalues.Pour résumer, le constructeur de copie effectue une copie complète, car la source doit rester intacte. Le constructeur de déplacement, d'autre part, peut simplement copier le pointeur, puis définir le pointeur dans la source sur null. Il est correct de "annuler" l'objet source de cette manière, car le client n'a aucun moyen d'inspecter l'objet à nouveau.
J'espère que cet exemple a permis de faire passer le message principal. Il y a beaucoup plus à réévaluer les références et à déplacer la sémantique que j'ai intentionnellement omis pour rester simple. Si vous souhaitez plus de détails, veuillez consulter ma réponse supplémentaire .
la source
that.data = 0
, les personnages seraient détruits bien trop tôt (à la mort temporaire), et aussi deux fois. Vous voulez voler les données, pas les partager!delete[]
sur un nullptr est défini par le standard C ++ comme un no-op.Ma première réponse a été une introduction extrêmement simplifiée pour déplacer la sémantique, et de nombreux détails ont été laissés exprès pour rester simple. Cependant, il y a beaucoup plus à faire pour déplacer la sémantique, et je pensais qu'il était temps pour une deuxième réponse de combler les lacunes. La première réponse est déjà assez ancienne et il ne semblait pas juste de la remplacer simplement par un texte complètement différent. Je pense que cela sert toujours bien de première introduction. Mais si vous voulez approfondir, lisez la suite :)
Stephan T. Lavavej a pris le temps de fournir de précieux commentaires. Merci beaucoup, Stephan!
introduction
La sémantique de déplacement permet à un objet, sous certaines conditions, de s'approprier les ressources externes d'un autre objet. Ceci est important de deux manières:
Transformer des copies coûteuses en mouvements bon marché. Voir ma première réponse pour un exemple. Notez que si un objet ne gère pas au moins une ressource externe (directement ou indirectement via ses objets membres), la sémantique de déplacement n'offre aucun avantage par rapport à la sémantique de copie. Dans ce cas, copier un objet et déplacer un objet signifie exactement la même chose:
Implémentation de types sécurisés «de déplacement uniquement»; c'est-à-dire les types pour lesquels la copie n'a pas de sens, mais le déplacement le fait. Les exemples incluent les verrous, les descripteurs de fichiers et les pointeurs intelligents avec une sémantique de propriété unique. Remarque: Cette réponse traite d'
std::auto_ptr
un modèle de bibliothèque standard C ++ 98 obsolète, qui a été remplacé parstd::unique_ptr
C ++ 11. Les programmeurs C ++ intermédiaires sont probablement au moins assez familiers etstd::auto_ptr
, en raison de la "sémantique de déplacement" qu'il affiche, cela semble être un bon point de départ pour discuter de la sémantique de déplacement en C ++ 11. YMMV.Qu'est-ce qu'un déménagement?
La bibliothèque standard C ++ 98 propose un pointeur intelligent avec une sémantique de propriété unique appelée
std::auto_ptr<T>
. Si vous n'êtes pas familierauto_ptr
, son but est de garantir qu'un objet alloué dynamiquement est toujours libéré, même en cas d'exceptions:Ce qui est inhabituel,
auto_ptr
c'est son comportement de "copie":Notez que l'initialisation de
b
witha
ne copie pas le triangle, mais transfère à la place la propriété du triangle dea
àb
. Nous disons également "a
est déplacé versb
" ou "le triangle est déplacé dea
versb
". Cela peut sembler déroutant car le triangle lui-même reste toujours au même endroit dans la mémoire.Le constructeur de copie de
auto_ptr
ressemble probablement à quelque chose comme ceci (quelque peu simplifié):Mouvements dangereux et inoffensifs
Ce qui est dangereux,
auto_ptr
c'est que ce qui ressemble syntaxiquement à une copie est en fait un mouvement. Essayer d'appeler une fonction membre sur un élément déplacéauto_ptr
invoquera un comportement indéfini, vous devez donc être très prudent de ne pas utiliser un élémentauto_ptr
après son déplacement:Mais ce
auto_ptr
n'est pas toujours dangereux. Les fonctions d'usine sont un cas d'utilisation parfaitement adapté pourauto_ptr
:Notez comment les deux exemples suivent le même modèle syntaxique:
Et pourtant, l'un d'eux invoque un comportement indéfini, tandis que l'autre ne le fait pas. Quelle est donc la différence entre les expressions
a
etmake_triangle()
? Ne sont-ils pas tous les deux du même type? En effet, ils le sont, mais ils ont des catégories de valeurs différentes .Catégories de valeur
De toute évidence, il doit y avoir une différence profonde entre l'expression
a
qui dénote uneauto_ptr
variable et l'expressionmake_triangle()
qui dénote l'appel d'une fonction qui renvoie uneauto_ptr
valeur par, créant ainsi un nouvelauto_ptr
objet temporaire à chaque appel.a
est un exemple de valeur l , alors quemake_triangle()
c'est un exemple de valeur r .Passer de lvalues telles que
a
est dangereux, car nous pourrions plus tard essayer d'appeler une fonction membre viaa
, en invoquant un comportement non défini. D'un autre côté, passer de rvalues telles quemake_triangle()
est parfaitement sûr, car une fois que le constructeur de copie a fait son travail, nous ne pouvons plus utiliser le temporaire. Il n'y a aucune expression qui dénote ledit temporaire; si nous écrivons simplement àmake_triangle()
nouveau, nous obtenons un autre temporaire. En fait, le temporaire déplacé est déjà parti sur la ligne suivante:Notez que les lettres
l
etr
ont une origine historique dans le côté gauche et le côté droit d'une affectation. Ce n'est plus le cas en C ++, car il y a des valeurs qui ne peuvent pas apparaître sur le côté gauche d'une affectation (comme des tableaux ou des types définis par l'utilisateur sans opérateur d'affectation), et il y a des rvalues qui peuvent (toutes les rvalues des types de classe avec un opérateur d'affectation).Références de valeur
Nous comprenons maintenant que passer de lvalues est potentiellement dangereux, mais passer de rvalues est inoffensif. Si C ++ avait un support de langage pour distinguer les arguments lvalue des arguments rvalue, nous pourrions soit interdire complètement le déplacement à partir de lvalues, soit au moins rendre explicite le déplacement à partir de lvalues sur le site d'appel, afin de ne plus bouger par accident.
La réponse de C ++ 11 à ce problème est les références rvalue . Une référence rvalue est un nouveau type de référence qui se lie uniquement aux rvalues, et la syntaxe est
X&&
. La bonne vieille référenceX&
est maintenant connue sous le nom de référence lvalue . (Notez que ceX&&
n'est pas une référence à une référence; il n'y a rien de tel en C ++.)Si nous jetons
const
dans le mélange, nous avons déjà quatre types de références différents. À quels types d'expressions de typeX
peuvent-ils se lier?En pratique, vous pouvez oublier
const X&&
. Restreindre à lire à partir des valeurs r n'est pas très utile.Conversions implicites
Les références Rvalue sont passées par plusieurs versions. Depuis la version 2.1, une référence rvalue
X&&
se lie également à toutes les catégories de valeurs d'un type différentY
, à condition qu'il y ait une conversion implicite deY
àX
. Dans ce cas, un temporaire de typeX
est créé et la référence rvalue est liée à ce temporaire:Dans l'exemple ci-dessus,
"hello world"
est une valeur l de typeconst char[12]
. Puisqu'il y a une conversion implicite deconst char[12]
àconst char*
enstd::string
, un temporaire de typestd::string
est créé etr
est lié à ce temporaire. C'est l'un des cas où la distinction entre rvalues (expressions) et temporelles (objets) est un peu floue.Déplacer les constructeurs
Un exemple utile de fonction avec un
X&&
paramètre est le constructeur de déplacementX::X(X&& source)
. Son objectif est de transférer la propriété de la ressource gérée de la source à l'objet actuel.En C ++ 11,
std::auto_ptr<T>
a été remplacé parstd::unique_ptr<T>
qui tire parti des références rvalue. Je développerai et discuterai d'une version simplifiée deunique_ptr
. Tout d'abord, nous encapsulons un pointeur brut et surchargeons les opérateurs->
et*
, par conséquent, notre classe se sent comme un pointeur:Le constructeur prend possession de l'objet et le destructeur le supprime:
Vient maintenant la partie intéressante, le constructeur de mouvement:
Ce constructeur de déplacement fait exactement ce que le
auto_ptr
constructeur de copie a fait, mais il ne peut être fourni qu'avec des rvalues:La deuxième ligne ne parvient pas à compiler, car il
a
s'agit d'une valeur l, mais le paramètreunique_ptr&& source
ne peut être lié qu'à des valeurs r. C'est exactement ce que nous voulions; les mouvements dangereux ne devraient jamais être implicites. La troisième ligne se compile très bien, car ilmake_triangle()
s'agit d'une valeur r. Le constructeur du déménagement transférera la propriété du temporaire auc
. Encore une fois, c'est exactement ce que nous voulions.Déplacer les opérateurs d'affectation
La dernière pièce manquante est l'opérateur d'affectation de déplacement. Son travail consiste à libérer l'ancienne ressource et à acquérir la nouvelle ressource à partir de son argument:
Notez comment cette implémentation de l'opérateur d'affectation de déplacement duplique la logique du destructeur et du constructeur de déplacement. Connaissez-vous l'idiome de copie et d'échange? Il peut également être appliqué pour déplacer la sémantique en tant qu'idiome de déplacement et d'échange:
Maintenant que
source
c'est une variable de typeunique_ptr
, elle sera initialisée par le constructeur de déplacement; c'est-à-dire que l'argument sera déplacé dans le paramètre. L'argument doit toujours être une valeur r, car le constructeur de déplacement lui-même a un paramètre de référence rvalue. Lorsque le flux de contrôle atteint l'accolade de fermeture deoperator=
,source
sort du champ d'application, libérant automatiquement l'ancienne ressource.Passer de lvalues
Parfois, nous voulons passer de lvalues. Autrement dit, nous voulons parfois que le compilateur traite une valeur l comme s'il s'agissait d'une valeur r, de sorte qu'il peut invoquer le constructeur de déplacement, même s'il peut être potentiellement dangereux. À cette fin, C ++ 11 propose un modèle de fonction de bibliothèque standard appelé
std::move
à l'intérieur de l'en-tête<utility>
. Ce nom est un peu malheureux, carstd::move
il jette simplement une valeur l à une valeur r; il ne pas bouger quoi que ce soit par lui - même. Il permet simplement de bouger. Peut-être qu'il aurait dû être nomméstd::cast_to_rvalue
oustd::enable_move
, mais nous sommes coincés avec le nom maintenant.Voici comment vous passez explicitement d'une valeur l:
Notez qu'après la troisième ligne,
a
ne possède plus de triangle. Ce n'est pas grave, car en écrivant explicitementstd::move(a)
, nous avons clairement exprimé nos intentions: "Cher constructeur, faites tout ce que vous vouleza
pour initialiserc
; je m'en fichea
plus. N'hésitez pas à vous débrouillera
."Xvalues
Notez que même s'il
std::move(a)
s'agit d'une valeur r, son évaluation ne crée pas d'objet temporaire. Cette énigme a forcé le comité à introduire une troisième catégorie de valeur. Quelque chose qui peut être lié à une référence rvalue, même s'il ne s'agit pas d'une valeur r au sens traditionnel, est appelé une valeur x (valeur eXpiring). Les valeurs traditionnelles ont été renommées en valeurs (valeurs pures).Les valeurs pr et x sont des valeurs r. Les valeurs X et les valeurs l sont toutes deux des valeurs gl (valeurs l généralisées). Les relations sont plus faciles à saisir avec un diagramme:
Notez que seules les valeurs x sont vraiment nouvelles; le reste est simplement dû au changement de nom et au regroupement.
Sortir des fonctions
Jusqu'à présent, nous avons vu des mouvements dans des variables locales et dans des paramètres de fonction. Mais le déplacement est également possible en sens inverse. Si une fonction renvoie par valeur, un objet sur le site d'appel (probablement une variable locale ou temporaire, mais pourrait être n'importe quel type d'objet) est initialisé avec l'expression après l'
return
instruction comme argument pour le constructeur de déplacement:De manière surprenante, les objets automatiques (variables locales qui ne sont pas déclarées comme
static
) peuvent également être implicitement retirés des fonctions:Comment se fait-il que le constructeur de mouvement accepte la valeur l
result
comme argument? La portée deresult
est sur le point de se terminer et elle sera détruite lors du déroulement de la pile. Personne ne pouvait se plaindre par la suite quiresult
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 a une règle spéciale qui permet de retourner des objets automatiques à partir de fonctions sans avoir à écrirestd::move
. En fait, vous ne devez jamais utiliserstd::move
pour déplacer des objets automatiques hors des fonctions, car cela inhibe l '"optimisation de la valeur de retour nommée" (NRVO).Notez que dans les deux fonctions d'usine, le type de retour est une valeur, pas une référence rvalue. Les références Rvalue sont toujours des références, et comme toujours, vous ne devez jamais renvoyer une référence à un objet automatique; l'appelant se retrouverait avec une référence pendant si vous incitiez le compilateur à accepter votre code, comme ceci:
Entrer dans les membres
Tôt ou tard, vous allez écrire du code comme ceci:
Fondamentalement, le compilateur se plaindra qu'il
parameter
s'agit d'une valeur l. Si vous regardez son type, vous voyez une référence rvalue, mais une référence rvalue signifie simplement "une référence qui est liée à une rvalue"; cela ne signifie pas que la référence elle-même est une valeur! En effet,parameter
c'est juste une variable ordinaire avec un nom. Vous pouvez utiliserparameter
autant de fois que vous le souhaitez à l'intérieur du corps du constructeur, et il désigne toujours le même objet. S'en éloigner implicitement serait dangereux, d'où la langue l'interdit.La solution consiste à activer manuellement le déplacement:
On pourrait dire que ce
parameter
n'est plus utilisé après l'initialisation demember
. Pourquoi n'y a-t-il pas de règle spéciale à insérer silencieusementstd::move
comme pour les valeurs de retour? Probablement parce que ce serait trop lourd pour les implémenteurs du compilateur. Par exemple, que se passe-t-il si le corps du constructeur se trouve dans une autre unité de traduction? En revanche, la règle de valeur de retour doit simplement vérifier les tables de symboles pour déterminer si l'identifiant après lereturn
mot - clé désigne un objet automatique.Vous pouvez également transmettre la
parameter
valeur par. Pour les types à déplacement uniquement commeunique_ptr
, il semble qu'il n'y ait pas encore d'idiome établi. Personnellement, je préfère passer par valeur, car cela provoque moins d'encombrement dans l'interface.Fonctions spéciales des membres
C ++ 98 déclare implicitement trois fonctions membres spéciales à la demande, c'est-à-dire lorsqu'elles sont nécessaires quelque part: le constructeur de copie, l'opérateur d'affectation de copie et le destructeur.
Les références Rvalue sont passées par plusieurs versions. Depuis la version 3.0, C ++ 11 déclare à la demande deux fonctions membres spéciales supplémentaires: le constructeur de déplacement et l'opérateur d'affectation de déplacement. Notez que ni VC10 ni VC11 ne sont encore conformes à la version 3.0, vous devrez donc les implémenter vous-même.
Ces deux nouvelles fonctions membres spéciaux ne sont déclarées implicitement que si aucune des fonctions membres spéciales n'est déclarée manuellement. De plus, si vous déclarez votre propre constructeur de déplacement ou opérateur d'affectation de déplacement, ni le constructeur de copie ni l'opérateur d'affectation de copie ne seront déclarés implicitement.
Que signifient ces règles dans la pratique?
Notez que l'opérateur d'affectation de copie et l'opérateur d'affectation de déplacement peuvent être fusionnés en un seul opérateur d'affectation unifié, en prenant son argument par valeur:
De cette façon, le nombre de fonctions membres spéciales à implémenter passe de cinq à quatre. Il y a un compromis à faire entre sécurité d'exception et efficacité ici, mais je ne suis pas un expert en la matière.
Renvoi de références ( précédemment appelées références universelles )
Considérez le modèle de fonction suivant:
Vous pouvez vous attendre
T&&
à ne vous lier qu'à rvalues, car à première vue, cela ressemble à une référence rvalue. Comme il s'avère cependant,T&&
se lie également aux valeurs l:Si l'argument est une valeur r de type
X
,T
se déduit êtreX
, doncT&&
moyenX&&
. C'est ce à quoi tout le monde s'attendrait. Mais si l'argument est une lvalue de typeX
, en raison d'une règle spéciale,T
on en déduit êtreX&
, par conséquentT&&
signifierait quelque chose commeX& &&
. Mais puisque C ++ n'a toujours aucune notion de références aux références, le typeX& &&
est réduit enX&
. Cela peut sembler déroutant et inutile au début, mais l'effondrement des références est essentiel pour une transmission parfaite (qui ne sera pas discutée ici).Si vous souhaitez contraindre un modèle de fonction à rvalues, vous pouvez combiner SFINAE avec des traits de type:
Mise en œuvre du déménagement
Maintenant que vous comprenez l'effondrement des références, voici comment
std::move
est implémenté:Comme vous pouvez le voir,
move
accepte tout type de paramètre grâce à la référence de transfertT&&
et renvoie une référence rvalue. L'std::remove_reference<T>::type
appel de la méta-fonction est nécessaire car sinon, pour les valeurs de typeX
, le type de retour seraitX& &&
, ce qui s'effondreraitX&
. Étant donné qu'ilt
s'agit toujours d'une valeur l (rappelez-vous qu'une référence rvalue nommée est une valeur l), mais que nous voulons nous liert
à une référence rvalue, nous devons explicitement convertirt
le type de retour correct. L'appel d'une fonction qui renvoie une référence rvalue est lui-même une valeur x. Vous savez maintenant d'où viennent les valeurs x;)Notez que le retour par référence rvalue est correct dans cet exemple, car
t
ne dénote pas un objet automatique, mais plutôt un objet qui a été transmis par l'appelant.la source
Les sémantiques de déplacement sont basées sur des références rvalue .
Une rvalue est un objet temporaire, qui va être détruit à la fin de l'expression. Dans le C ++ actuel, les valeurs r ne se lient qu'aux
const
références. C ++ 1x autorise lesconst
références non rvalue, orthographiéesT&&
, qui sont des références à des objets rvalue.Puisqu'une valeur r va mourir à la fin d'une expression, vous pouvez voler ses données . Au lieu de le copier dans un autre objet, vous y déplacez ses données.
Dans le code ci - dessus, avec d' anciens compilateurs le résultat de
f()
est copié enx
utilisantX
de » constructeur de copie. Si votre compilateur prend en charge la sémantique de déplacement etX
a un constructeur de déplacement, alors cela est appelé à la place. Puisque sonrhs
argument est une valeur r , nous savons que ce n'est plus nécessaire et nous pouvons voler sa valeur.Ainsi, la valeur est déplacée du temporaire sans nom renvoyé de
f()
àx
(tandis que les données dex
, initialisées sur un videX
, sont déplacées dans le temporaire, qui sera détruit après l'affectation).la source
this->swap(std::move(rhs));
parce que les références nommées rvalue sont lvaluesrhs
est une lvalue dans le contexte deX::X(X&& rhs)
. Vous devez appelerstd::move(rhs)
pour obtenir une valeur r, mais cela rend la réponse théorique.Supposons que vous ayez une fonction qui renvoie un objet substantiel:
Lorsque vous écrivez du code comme celui-ci:
puis un compilateur C ++ ordinaire créera un objet temporaire pour le résultat de
multiply()
, appellera le constructeur de copie pour initialiserr
, puis détruira la valeur de retour temporaire. Les sémantiques de déplacement en C ++ 0x permettent au "constructeur de déplacement" d'être appelé pour s'initialiserr
en copiant son contenu, puis de supprimer la valeur temporaire sans avoir à la détruire.Cela est particulièrement important si (comme peut-être l'
Matrix
exemple ci-dessus), l'objet copié alloue de la mémoire supplémentaire sur le tas pour stocker sa représentation interne. Un constructeur de copie devrait soit faire une copie complète de la représentation interne, soit utiliser le comptage des références et la sémantique de copie sur écriture en interne. Un constructeur de déplacement laisserait la mémoire de tas seule et copierait simplement le pointeur à l'intérieur de l'Matrix
objet.la source
Si vous êtes vraiment intéressé par une bonne explication approfondie de la sémantique des mouvements, je vous recommande fortement de lire le document original à leur sujet, «Une proposition pour ajouter le support de la sémantique des mouvements au langage C ++».
Il est très accessible et facile à lire et il constitue un excellent argument pour les avantages qu'ils offrent. Il existe d'autres articles plus récents et à jour sur la sémantique des mouvements disponibles sur le site Web du WG21 , mais celui-ci est probablement le plus simple car il aborde les choses d'une vue de haut niveau et n'entre pas beaucoup dans les détails du langage.
la source
Déplacer la sémantique consiste à transférer des ressources plutôt qu'à les copier lorsque personne n'a plus besoin de la valeur source.
En C ++ 03, les objets sont souvent copiés, uniquement pour être détruits ou affectés avant qu'un code n'utilise à nouveau la valeur. Par exemple, lorsque vous retournez par valeur à partir d'une fonction - sauf si RVO entre en action - la valeur que vous renvoyez est copiée dans le cadre de pile de l'appelant, puis elle sort du domaine et est détruite. Ce n'est qu'un exemple parmi tant d'autres: voir la valeur de passage lorsque l'objet source est temporaire, des algorithmes comme
sort
celui-ci ne font que réorganiser les éléments, la réallocation envector
cas decapacity()
dépassement, etc.Lorsque de telles paires copier / détruire coûtent cher, c'est généralement parce que l'objet possède une ressource lourde. Par exemple,
vector<string>
peut posséder un bloc de mémoire alloué dynamiquement contenant un tableau d'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 à travers. Ensuite, vous devez libérer toute la mémoire que vous venez de copier. Cependant, déplacer un grandvector<string>
signifie simplement copier quelques pointeurs (qui font référence au bloc de mémoire dynamique) vers la destination et les mettre à zéro dans la source.la source
En termes simples (pratiques):
Copier un objet signifie copier ses membres "statiques" et appeler l'
new
opérateur pour ses objets dynamiques. Droite?Cependant, déplacer un objet (je le répète, d'un point de vue pratique) implique seulement de copier les pointeurs des objets dynamiques, et non d'en créer de nouveaux.
Mais n'est-ce pas dangereux? Bien sûr, vous pourriez détruire deux fois un objet dynamique (défaut de segmentation). Donc, pour éviter cela, vous devez "invalider" les pointeurs source pour éviter de les détruire deux fois:
D'accord, mais si je déplace un objet, l'objet source devient inutile, non? Bien sûr, mais dans certaines situations, c'est très utile. Le plus évident est lorsque j'appelle une fonction avec un objet anonyme (temporel, objet rvalue, ..., vous pouvez l'appeler avec des noms différents):
Dans cette situation, un objet anonyme est créé, ensuite copié dans le paramètre de fonction, puis supprimé. Donc, ici, il est préférable de déplacer l'objet, car vous n'avez pas besoin de l'objet anonyme et vous pouvez économiser du temps et de la mémoire.
Cela conduit au concept d'une référence "rvalue". Ils existent en C ++ 11 uniquement pour détecter si l'objet reçu est anonyme ou non. Je pense que vous savez déjà qu'une "lvalue" est une entité assignable (la partie gauche de l'
=
opérateur), donc vous avez besoin d'une référence nommée à un objet pour pouvoir agir comme une lvalue. Une valeur r est exactement le contraire, un objet sans références nommées. Pour cette raison, objet anonyme et rvalue sont synonymes. Donc:Dans ce cas, lorsqu'un objet de type
A
doit être "copié", le compilateur crée une référence lvalue ou une référence rvalue selon que l'objet passé est nommé ou non. Sinon, votre constructeur de déplacement est appelé et vous savez que l'objet est temporel et vous pouvez déplacer ses objets dynamiques au lieu de les copier, économisant ainsi de l'espace et de la mémoire.Il est important de se rappeler que les objets "statiques" sont toujours copiés. Il n'y a aucun moyen de "déplacer" un objet statique (objet dans la pile et non sur le tas). Ainsi, la distinction "déplacer" / "copier" lorsqu'un objet n'a pas de membres dynamiques (directement ou indirectement) n'est pas pertinente.
Si votre objet est complexe et que le destructeur a d'autres effets secondaires, comme appeler la fonction d'une bibliothèque, appeler d'autres fonctions globales ou quoi que ce soit, il vaut peut-être mieux signaler un mouvement avec un drapeau:
Ainsi, votre code est plus court (vous n'avez pas besoin de faire une
nullptr
affectation pour chaque membre dynamique) et plus général.Autre question typique: quelle est la différence entre
A&&
etconst A&&
? Bien sûr, dans le premier cas, vous pouvez modifier l'objet et dans le second non, mais, sens pratique? Dans le second cas, vous ne pouvez pas le modifier, vous n'avez donc aucun moyen d'invalider l'objet (sauf avec un indicateur mutable ou quelque chose comme ça), et il n'y a aucune différence pratique pour un constructeur de copie.Et quelle est la transmission parfaite ? Il est important de savoir qu'une "référence rvalue" est une référence à un objet nommé dans la "portée de l'appelant". Mais dans la portée réelle, une référence rvalue est un nom à un objet, elle agit donc comme un objet nommé. Si vous transmettez une référence rvalue à une autre fonction, vous passez un objet nommé, donc, l'objet n'est pas reçu comme un objet temporel.
L'objet
a
serait copié dans le paramètre réel deother_function
. Si vous voulez que l'objeta
continue à être traité comme un objet temporaire, vous devez utiliser lastd::move
fonction:Avec cette ligne,
std::move
seraa
converti en une valeur r etother_function
recevra l'objet en tant qu'objet sans nom. Bien sûr, s'ilother_function
n'y a pas de surcharge spécifique pour travailler avec des objets sans nom, cette distinction n'est pas importante.Est-ce une transmission parfaite? Non, mais nous sommes très proches. Un transfert parfait n'est utile que pour travailler avec des modèles, dans le but de dire: si j'ai besoin de passer un objet à une autre fonction, j'ai besoin que si je reçois un objet nommé, l'objet soit passé en tant qu'objet nommé, et dans le cas contraire, Je veux le passer comme un objet sans nom:
C'est la signature d'une fonction prototypique qui utilise une transmission parfaite, implémentée en C ++ 11 au moyen de
std::forward
. Cette fonction exploite certaines règles d'instanciation de modèle:Donc, si
T
est une référence lvalue àA
( T = A &),a
aussi ( A & && => A &). SiT
est une référence de valeur àA
,a
également (A && && => A &&). Dans les deux cas,a
est un objet nommé dans la portée réelle, maisT
contient les informations de son "type de référence" du point de vue de la portée de l'appelant. Ces informations (T
) sont transmises en tant que paramètre de modèle àforward
et «a» est déplacé ou non selon le type deT
.la source
C'est comme copier la sémantique, mais au lieu d'avoir à dupliquer toutes les données, vous obtenez de voler les données de l'objet à "déplacer".
la source
Vous savez ce que signifie une sémantique de copie, n'est-ce pas? cela signifie que vous avez des types qui sont copiables, pour les types définis par l'utilisateur, vous définissez cela soit en écrivant explicitement un constructeur de copie et un opérateur d'affectation, soit le compilateur les génère implicitement. Cela fera une copie.
La sémantique de déplacement est fondamentalement un type défini par l'utilisateur avec un constructeur qui prend une référence de valeur r (nouveau type de référence utilisant && (oui deux esperluettes)) qui n'est pas const, cela s'appelle un constructeur de déplacement, il en va de même pour l'opérateur d'affectation. Alors, que fait un constructeur de déplacement, au lieu de copier la mémoire de son argument source, il «déplace» la mémoire de la source vers la destination.
Quand voudriez-vous faire ça? bien std :: vector est un exemple, disons que vous avez créé un std :: vector temporaire et que vous le retournez à partir d'une fonction, dites:
Vous allez avoir des frais généraux du constructeur de copie lorsque la fonction revient, si (et il le fera en C ++ 0x) std :: vector a un constructeur de déplacement au lieu de le copier, il peut simplement définir ses pointeurs et «déplacer» alloué dynamiquement mémoire à la nouvelle instance. C'est un peu comme la sémantique du transfert de propriété avec std :: auto_ptr.
la source
Pour illustrer le besoin de déplacer la sémantique , considérons cet exemple sans déplacer la sémantique:
Voici une fonction qui prend un objet de type
T
et renvoie un objet du même typeT
:La fonction ci-dessus utilise l' appel par valeur, ce qui signifie que lorsque cette fonction est appelée, un objet doit être construit pour être utilisé par la fonction.
Étant donné que la fonction renvoie également par valeur , un autre nouvel objet est construit pour la valeur de retour:
Deux nouveaux objets ont été construits, dont l'un est un objet temporaire qui n'est utilisé que pour la durée de la fonction.
Lorsque le nouvel objet est créé à partir de la valeur de retour, le constructeur de copie est appelé pour copier le contenu de l'objet temporaire vers le nouvel objet b. Une fois la fonction terminée, l'objet temporaire utilisé dans la fonction sort du cadre et est détruit.
Voyons maintenant ce que fait un constructeur de copie .
Il doit d'abord initialiser l'objet, puis copier toutes les données pertinentes de l'ancien objet vers le nouveau.
Selon la classe, c'est peut-être un conteneur avec beaucoup de données, alors cela pourrait représenter beaucoup de temps et d' utilisation de la mémoire
Avec la sémantique de déplacement, il est désormais possible de rendre la plupart de ce travail moins désagréable en déplaçant simplement les données plutôt qu'en les copiant.
Le déplacement des données implique de réassocier les données au nouvel objet. Et aucune copie n'a lieu du tout.
Ceci est accompli avec une
rvalue
référence.Une
rvalue
référence fonctionne à peu près comme unelvalue
référence avec une différence importante:une référence rvalue peut être déplacée et une lvalue ne peut pas.
De cppreference.com :
la source
J'écris ceci pour m'assurer de bien le comprendre.
La sémantique de déplacement a été créée pour éviter la copie inutile de gros objets. Bjarne Stroustrup dans son livre "The C ++ Programming Language" utilise deux exemples où la copie inutile se produit par défaut: un, l'échange de deux gros objets, et deux, le retour d'un grand objet d'une méthode.
L'échange de deux grands objets implique généralement la copie du premier objet dans un objet temporaire, la copie du deuxième objet dans le premier objet et la copie de l'objet temporaire dans le deuxième objet. Pour un type intégré, c'est très rapide, mais pour les gros objets, ces trois copies peuvent prendre beaucoup de temps. Une "affectation de déplacement" permet au programmeur de remplacer le comportement de copie par défaut et de remplacer les références aux objets, ce qui signifie qu'il n'y a aucune copie du tout et que l'opération de permutation est beaucoup plus rapide. L'affectation de déplacement peut être invoquée en appelant la méthode std :: move ().
Le retour d'un objet d'une méthode par défaut implique de faire une copie de l'objet local et de ses données associées dans un emplacement accessible à l'appelant (car l'objet local n'est pas accessible à l'appelant et disparaît lorsque la méthode se termine). Lorsqu'un type intégré est renvoyé, cette opération est très rapide, mais si un objet volumineux est renvoyé, cela peut prendre du temps. Le constructeur de déplacement permet au programmeur de remplacer ce comportement par défaut et de "réutiliser" les données de tas associées à l'objet local en pointant l'objet renvoyé à l'appelant pour stocker les données associées à l'objet local. Ainsi, aucune copie n'est requise.
Dans les langages qui ne permettent pas la création d'objets locaux (c'est-à-dire les objets de la pile), ces types de problèmes ne se produisent pas car tous les objets sont alloués sur le tas et sont toujours accessibles par référence.
la source
x
ety
, vous ne pouvez pas simplement "échanger des références aux objets" ; il se peut que les objets contiennent des pointeurs qui référencent d'autres données, et ces pointeurs peuvent être échangés, mais les opérateurs de déplacement ne sont pas tenus de permuter quoi que ce soit. Ils peuvent effacer les données de l'objet déplacé, plutôt que de conserver les données de destination qui s'y trouvent.swap()
sans déplacer la sémantique. "L'affectation de déplacement peut être invoquée en appelant la méthode std :: move ()." - il est parfois nécessaire d'utiliserstd::move()
- bien que cela ne déplace rien - laisse simplement le compilateur savoir que l'argument est mobile, parfoisstd::forward<>()
(avec des références de transfert), et d'autres fois le compilateur sait qu'une valeur peut être déplacée.Voici une réponse du livre "The C ++ Programming Language" de Bjarne Stroustrup. Si vous ne voulez pas voir la vidéo, vous pouvez voir le texte ci-dessous:
Considérez cet extrait. Le retour d'un opérateur + implique de copier le résultat hors de la variable locale
res
et dans un endroit où l'appelant peut y accéder.Nous ne voulions pas vraiment de copie; nous voulions juste obtenir le résultat d'une fonction. Nous devons donc déplacer un vecteur plutôt que de le copier. Nous pouvons définir le constructeur de déplacement comme suit:
Le && signifie "rvalue reference" et est une référence à laquelle nous pouvons lier une rvalue. "rvalue" 'est destiné à compléter "lvalue" qui signifie à peu près "quelque chose qui peut apparaître sur le côté gauche d'une affectation". Ainsi, une valeur r signifie à peu près "une valeur à laquelle vous ne pouvez pas affecter", comme un entier renvoyé par un appel de fonction, et la
res
variable locale dans l'opérateur + () pour les vecteurs.Maintenant, la déclaration
return res;
ne sera pas copiée!la source