Dans l'opérateur d'affectation d'une classe, vous devez généralement vérifier si l'objet affecté est l'objet appelant pour ne pas gâcher les choses:
Class& Class::operator=(const Class& rhs) {
if (this != &rhs) {
// do the assignment
}
return *this;
}
Avez-vous besoin de la même chose pour l'opérateur d'affectation de déplacement? Y a-t-il jamais eu une situation où ce this == &rhs
serait vrai?
? Class::operator=(Class&& rhs) {
?
}
c++
c++11
move-semantics
move-assignment-operator
Seth Carnegie
la source
la source
A a; a = std::move(a);
.std::move
est normale. Ensuite, tenez compte de l'aliasing, et lorsque vous êtes profondément dans une pile d'appels et que vous avez une référence àT
, et une autre référence àT
... allez-vous vérifier l'identité ici? Voulez-vous trouver le premier appel (ou les appels) où documenter que vous ne pouvez pas passer le même argument deux fois prouvera statiquement que ces deux références ne seront pas alias? Ou allez-vous faire fonctionner l'auto-affectation?std::sort
oustd::shuffle
- à chaque fois que vous échangez lesi
e etj
e éléments d'un tableau sans vérifier aui != j
préalable. (std::swap
est implémenté en termes d'attribution de déplacement.)Réponses:
Wow, il y a tellement de choses à nettoyer ici ...
Premièrement, la copie et l'échange ne sont pas toujours la bonne manière d'implémenter l'affectation de copie. Presque certainement dans le cas de
dumb_array
, c'est une solution sous-optimale.L'utilisation de Copy and Swap est
dumb_array
un exemple classique de placement de l'opération la plus coûteuse avec les fonctionnalités les plus complètes au niveau de la couche inférieure. Il est parfait pour les clients qui veulent la fonctionnalité la plus complète et sont prêts à payer la pénalité de performance. Ils obtiennent exactement ce qu'ils veulent.Mais c'est désastreux pour les clients qui n'ont pas besoin de la fonctionnalité la plus complète et recherchent plutôt les performances les plus élevées. Pour eux, il
dumb_array
n'y a qu'un autre logiciel qu'ils doivent réécrire parce qu'il est trop lent. Avaitdumb_array
été conçu différemment, il aurait pu satisfaire les deux clients sans compromis pour l'un ou l'autre client.La clé pour satisfaire les deux clients est de créer les opérations les plus rapides au niveau le plus bas, puis d'ajouter une API en plus de cela pour des fonctionnalités plus complètes à plus de frais. Ie vous avez besoin de la garantie d'exception forte, très bien, vous payez pour cela. Vous n'en avez pas besoin? Voici une solution plus rapide.
Soyons concrets: voici l'opérateur d'affectation de copie de garantie d'exception rapide et basique pour
dumb_array
:Explication:
L'une des choses les plus coûteuses que vous puissiez faire sur du matériel moderne est de vous déplacer vers le tas. Tout ce que vous pouvez faire pour éviter un voyage dans le tas est du temps et des efforts bien dépensés. Les clients de
dumb_array
peuvent souhaiter attribuer souvent des tableaux de même taille. Et quand ils le font, tout ce que vous avez à faire est unmemcpy
(caché sousstd::copy
). Vous ne voulez pas allouer un nouveau tableau de la même taille, puis désallouer l'ancien de la même taille!Maintenant, pour vos clients qui veulent réellement une sécurité d'exception forte:
Ou peut-être que si vous souhaitez profiter de l'affectation de déplacement en C ++ 11, cela devrait être:
Si
dumb_array
les clients apprécient la vitesse, ils doivent appeler leoperator=
. S'ils ont besoin d'une sécurité d'exception forte, ils peuvent appeler des algorithmes génériques qui fonctionneront sur une grande variété d'objets et ne devront être implémentés qu'une seule fois.Revenons maintenant à la question d'origine (qui a un type-o à ce stade):
C'est en fait une question controversée. Certains diront oui, absolument, certains diront non.
Mon opinion personnelle est non, vous n'avez pas besoin de cette vérification.
Raisonnement:
Lorsqu'un objet se lie à une référence rvalue, c'est l'une des deux choses suivantes:
Si vous avez une référence à un objet qui est un véritable temporaire, alors par définition, vous avez une référence unique à cet objet. Il ne peut être référencé nulle part ailleurs dans l'ensemble de votre programme. Ie
this == &temporary
n'est pas possible .Maintenant, si votre client vous a menti et vous a promis que vous obtiendrez un temporaire alors que vous ne l'êtes pas, il est de la responsabilité du client de s'assurer que vous n'avez pas à vous en soucier. Si vous voulez être vraiment prudent, je pense que ce serait une meilleure implémentation:
Autrement dit , si vous êtes passé une référence auto, c'est un bug de la part du client qui doit être fixé.
Pour être complet, voici un opérateur d'affectation de déplacement pour
dumb_array
:Dans le cas d'utilisation typique de l'affectation de déplacement,
*this
sera un objet déplacé etdelete [] mArray;
devrait donc être interdit. Il est essentiel que les implémentations effectuent la suppression sur un nullptr aussi rapidement que possible.Caveat:
Certains diront que
swap(x, x)
c'est une bonne idée, ou juste un mal nécessaire. Et cela, si le swap va au swap par défaut, peut provoquer une affectation de déplacement automatique.Je suis en désaccord que
swap(x, x)
est toujours une bonne idée. S'il se trouve dans mon propre code, je le considérerai comme un bogue de performance et le corrigerai. Mais au cas où vous voudriez l'autoriser,swap(x, x)
sachez que self-move-assignemnet ne fait que sur une valeur déplacée. Et dans notredumb_array
exemple, ce sera parfaitement inoffensif si nous omettons simplement l'assertion, ou la contraignons au cas déplacé:Si vous attribuez vous-même deux déplacés (vides)
dumb_array
, vous ne faites rien de mal à part insérer des instructions inutiles dans votre programme. Ce même constat peut être fait pour la grande majorité des objets.<
Mettre à jour>
J'ai réfléchi davantage à cette question et j'ai quelque peu changé ma position. Je crois maintenant que l'affectation devrait être tolérante à l'auto-affectation, mais que les conditions de publication sur l'affectation de copie et l'affectation de déménagement sont différentes:
Pour l'affectation de copie:
on devrait avoir une post-condition que la valeur de
y
ne devrait pas être modifiée. Quand&x == &y
alors cette post-condition se traduit par: l'affectation de copie automatique ne devrait pas avoir d'impact sur la valeur dex
.Pour l'attribution de déménagement:
on devrait avoir une post-condition qui
y
a un état valide mais non spécifié. Quand&x == &y
alors cette post-condition se traduit par:x
a un état valide mais non spécifié. C’est-à-dire que l’affectation d’auto-déménagement ne doit pas nécessairement être interdite. Mais il ne devrait pas s'écraser. Cette post-condition est cohérente pour permettreswap(x, x)
de simplement travailler:Ce qui précède fonctionne, tant
x = std::move(x)
qu'il ne plante pas. Il peut partirx
dans n'importe quel état valide mais non spécifié.Je vois trois façons de programmer l'opérateur d'affectation de déplacement pour
dumb_array
y parvenir:La mise en œuvre ci - dessus l' affectation automatique tolère, mais
*this
etother
finissent par être un tableau de taille zéro après la cession auto-move, quelle que soit la valeur d' origine*this
est. C'est bon.L'implémentation ci-dessus tolère l'auto-affectation de la même manière que l'opérateur d'affectation de copie, en en faisant un no-op. C'est bien aussi.
Ce qui précède n'est correct que s'il
dumb_array
ne contient pas de ressources qui doivent être détruites «immédiatement». Par exemple, si la seule ressource est la mémoire, ce qui précède convient. S'ildumb_array
pouvait contenir des verrous mutex ou l'état ouvert des fichiers, le client pouvait raisonnablement s'attendre à ce que ces ressources sur les lhs de l'affectation de déplacement soient immédiatement libérées et cette implémentation pourrait donc être problématique.Le coût du premier est de deux magasins supplémentaires. Le coût du second est un test et une branche. Les deux fonctionnent. Les deux répondent à toutes les exigences du tableau 22 des exigences MoveAssignable dans la norme C ++ 11. Le troisième fonctionne également modulo le problème des ressources sans mémoire.
Les trois implémentations peuvent avoir des coûts différents selon le matériel: quel est le coût d'une succursale? Y a-t-il beaucoup de registres ou très peu?
Ce qu'il faut retenir, c'est que l'affectation de déplacement automatique, contrairement à l'affectation de copie automatique, n'a pas à conserver la valeur actuelle.
<
/Mettre à jour>
Un dernier montage (espérons-le) inspiré du commentaire de Luc Danton:
Si vous écrivez une classe de haut niveau qui ne gère pas directement la mémoire (mais peut avoir des bases ou des membres qui le font), alors la meilleure implémentation de l'affectation de déplacement est souvent:
Cela déplacera chaque base et chaque membre à tour de rôle, et n'inclura pas de
this != &other
chèque. Cela vous donnera les performances les plus élevées et la sécurité des exceptions de base en supposant qu'aucun invariant ne doit être maintenu parmi vos bases et vos membres. Pour vos clients exigeant une sécurité d'exception forte, dirigez-les versstrong_assign
.la source
std::swap(x,x)
, alors pourquoi devrais-je lui faire confiance pour gérer correctement les opérations plus compliquées?std::swap(x,x)
va, cela fonctionne juste même lorsquex = std::move(x)
produit un résultat non spécifié. Essayez-le! Tu n'as pas besoin de me croire.swap
fonctionne aussi longtemps que lesx = move(x)
feuillesx
dans n'importe quel état de passage. Et les algorithmesstd::copy
/std::move
sont définis de manière à produire déjà un comportement indéfini sur les copies no-op (aïe; le jeune de 20 ansmemmove
obtient le bon cas maisstd::move
ne le fait pas!). Donc je suppose que je n'ai pas encore pensé à un "slam dunk" pour l'auto-affectation. Mais de toute évidence, l'auto-affectation est quelque chose qui se produit souvent dans le vrai code, que le Standard l'ait béni ou non.Tout d'abord, la signature de l'opérateur d'assignation de déplacement est erronée. Puisque les déplacements volent les ressources de l'objet source, la source doit être une
const
référence sans valeur r.Notez que vous retournez toujours via une référence (non
const
) l -value.Pour l'un ou l'autre type d'affectation directe, la norme n'est pas de vérifier l'auto-affectation, mais de s'assurer qu'une auto-affectation ne provoque pas de crash-and-burn. Généralement, personne ne fait
x = x
ouy = std::move(y)
n'appelle explicitement , mais l'aliasing, en particulier via plusieurs fonctions, peut conduirea = b
ouc = std::move(d)
devenir des auto-assignations. Une vérification explicite de l'auto-affectation, c'est-à-direthis == &rhs
qui saute la viande de la fonction lorsqu'elle est vraie, est un moyen d'assurer la sécurité de l'auto-assignation. Mais c'est l'un des pires moyens, car il optimise un cas rare (espérons-le), alors qu'il s'agit d'une anti-optimisation pour le cas le plus courant (en raison de branchements et éventuellement d'erreurs de cache).Maintenant, lorsque (au moins) l'un des opérandes est un objet directement temporaire, vous ne pouvez jamais avoir de scénario d'auto-affectation. Certaines personnes préconisent de supposer ce cas et d'optimiser le code à tel point que le code devient suicidement stupide lorsque l'hypothèse est fausse. Je dis que jeter le contrôle du même objet sur les utilisateurs est irresponsable. Nous ne faisons pas cet argument pour l'affectation de copie; pourquoi inverser la position pour l'attribution de mouvement?
Faisons un exemple, modifié par un autre répondant:
Cette affectation de copie gère l'auto-affectation de manière gracieuse sans vérification explicite. Si les tailles source et destination diffèrent, la désallocation et la réallocation précèdent la copie. Sinon, seule la copie est effectuée. L'auto-affectation n'obtient pas un chemin optimisé, elle est sauvegardée dans le même chemin que lorsque les tailles source et destination commencent égales. La copie est techniquement inutile lorsque les deux objets sont équivalents (y compris lorsqu'ils sont le même objet), mais c'est le prix à payer si vous ne faites pas de contrôle d'égalité (en valeur ou en adresse) car ledit contrôle lui-même serait un gaspillage le plus du temps. Notez que l'auto-affectation d'objet provoquera ici une série d'auto-affectations au niveau de l'élément; le type d'élément doit être sûr pour ce faire.
Comme son exemple source, cette affectation de copie fournit la garantie de sécurité d'exception de base. Si vous souhaitez bénéficier d'une garantie renforcée, utilisez l'opérateur d'affectation unifiée de la requête Copie et échange d'origine , qui gère à la fois l'affectation de copie et de déplacement. Mais le but de cet exemple est de réduire la sécurité d'un rang pour gagner en vitesse. (BTW, nous supposons que les valeurs des éléments individuels sont indépendantes; qu'il n'y a pas de contrainte invariante limitant certaines valeurs par rapport à d'autres.)
Regardons une affectation de déplacement pour ce même type:
Un type échangeable qui nécessite une personnalisation doit avoir une fonction sans deux arguments appelée
swap
dans le même espace de noms que le type. (La restriction d'espace de noms permet aux appels non qualifiés de permuter de fonctionner.) Un type de conteneur doit également ajouter une fonctionswap
membre public pour correspondre aux conteneurs standard. Si un membreswap
n'est pas fourni, alors la fonction libreswap
doit probablement être marquée comme un ami du type échangeable. Si vous personnalisez les mouvements à utiliserswap
, vous devez fournir votre propre code d'échange; le code standard appelle le code de déplacement du type, ce qui entraînerait une récurrence mutuelle infinie pour les types personnalisés par déplacement.Comme les destructeurs, les fonctions d'échange et les opérations de déplacement ne doivent jamais être lancées si possible, et probablement marquées comme telles (en C ++ 11). Les types de bibliothèques et les routines standard ont des optimisations pour les types de déplacement non jetables.
Cette première version de l'attribution de déménagement remplit le contrat de base. Les marqueurs de ressources de la source sont transférés vers l'objet de destination. Les anciennes ressources ne seront pas divulguées puisque l'objet source les gère désormais. Et l'objet source est laissé dans un état utilisable où d'autres opérations, y compris l'affectation et la destruction, peuvent lui être appliquées.
Notez que cette affectation de déplacement est automatiquement sûre pour l'auto-affectation, puisque l'
swap
appel l'est. C'est aussi fortement exceptionnellement sûr. Le problème est la rétention inutile des ressources. Les anciennes ressources pour la destination ne sont plus nécessaires du point de vue conceptuel, mais ici, elles ne sont toujours disponibles que pour que l'objet source puisse rester valide. Si la destruction programmée de l'objet source est loin, nous gaspillons de l'espace de ressources, ou pire si l'espace de ressources total est limité et que d'autres requêtes de ressources se produiront avant que le (nouvel) objet source ne meure officiellement.Ce problème est à l'origine des conseils controversés des gourous actuels concernant l'auto-ciblage lors de l'attribution des déplacements. La façon d'écrire une affectation de déplacement sans ressources persistantes est quelque chose comme:
La source est réinitialisée aux conditions par défaut, tandis que les anciennes ressources de destination sont détruites. Dans le cas de l'auto-affectation, votre objet actuel finit par se suicider. Le principal moyen de contourner ce problème est d'entourer le code d'action d'un
if(this != &other)
bloc, ou de le visser et de laisser les clients manger uneassert(this != &other)
première ligne (si vous vous sentez bien).Une alternative consiste à étudier comment rendre l'attribution de copie fortement sécurisée pour les exceptions, sans assignation unifiée, et l'appliquer à l'attribution de mouvement:
Quand
other
etthis
sont distincts,other
est vidé par le passage àtemp
et le reste. Puisthis
perd ses anciennes ressourcestemp
tout en récupérant les ressources initialement détenues parother
. Ensuite, les anciennes ressourcesthis
sont tuées quandtemp
.Lorsque l'auto-affectation se produit, le vidage de
other
verstemp
se videthis
également. Ensuite, l'objet cible récupère ses ressources quandtemp
etthis
échange. La mort detemp
réclame un objet vide, qui devrait être pratiquement un no-op. L' objetthis
/other
conserve ses ressources.L'assignation de mouvement ne doit jamais être lancée tant que la construction de mouvement et l'échange le sont également. Le coût d'être également en sécurité pendant l'auto-affectation est quelques instructions supplémentaires par rapport aux types de bas niveau, qui devraient être submergées par l'appel de désallocation.
la source
delete
votre deuxième bloc de code?std::copy
provoque un comportement indéfini si les plages source et destination se chevauchent (y compris le cas où elles coïncident). Voir C ++ 14 [alg.copy] / 3.Je suis dans le camp de ceux qui veulent des opérateurs sûrs pour l'auto-affectation, mais ne veulent pas écrire de chèques d'auto-affectation dans les implémentations de
operator=
. Et en fait, je ne veux même pas du tout mettreoperator=
en œuvre , je veux que le comportement par défaut fonctionne «dès la sortie de la boîte». Les meilleurs membres spéciaux sont ceux qui viennent gratuitement.Cela étant dit, les exigences MoveAssignable présentes dans la norme sont décrites comme suit (à partir de 17.6.3.1 Exigences d'argument de modèle [utility.arg.requirements], n3290):
où les espaces réservés sont décrits comme
t
suit : " [est une] valeur l modifiable de type T;" et "rv
est une rvaleur de type T;". Notez que ce sont des exigences placées sur les types utilisés comme arguments dans les modèles de la bibliothèque Standard, mais en regardant ailleurs dans la norme, je remarque que chaque exigence relative à l'affectation de déplacement est similaire à celle-ci.Cela signifie que cela
a = std::move(a)
doit être «sûr». Si vous avez besoin d'un test d'identité (par exemplethis != &other
), allez-y, sinon vous ne pourrez même pas y mettre vos objetsstd::vector
! (À moins que vous n'utilisiez les membres / opérations qui nécessitent MoveAssignable; mais ne le pensez pas.) Notez qu'avec l'exemple précédenta = std::move(a)
,this == &other
cela tiendra effectivement.la source
a = std::move(a)
ne pas travailler empêcherait une classe de travailler avecstd::vector
? Exemple?std::vector<T>::erase
n'est pas autorisé sauf siT
MoveAssignable. (En marge de l'IIRC, certaines exigences de MoveAssignable ont été assouplies pour MoveInsertable à la place en C ++ 14.)T
doit donc être MoveAssignable, mais pourquoierase()
dépendrait-il du déplacement d'un élément vers lui - même ?Pendant que votre
operator=
fonction actuelle est écrite, puisque vous avez fait l'argument rvalue-referenceconst
, il n'y a aucun moyen que vous puissiez "voler" les pointeurs et changer les valeurs de la référence rvalue entrante ... vous ne pouvez tout simplement pas le changer, vous ne pouvait que lire. Je ne verrais un problème que si vous deviez commencer à appeler desdelete
pointeurs, etc. dans votrethis
objet comme vous le feriez dans uneoperator=
méthode de référence lvaue normale , mais cela va à l'encontre du point de la rvalue-version ... c'est-à-dire que ce serait semble redondant d'utiliser la version rvalue pour effectuer fondamentalement les mêmes opérations normalement laissées à une méthodeconst
-lvalueoperator=
.Maintenant, si vous avez défini votre
operator=
pour prendre uneconst
référence non -rvalue, alors la seule façon pour moi de voir une vérification requise était de passer l'this
objet à une fonction qui renvoyait intentionnellement une référence rvalue plutôt qu'une référence temporaire.Par exemple, supposons que quelqu'un ait essayé d'écrire une
operator+
fonction et d'utiliser un mélange de références rvalue et de références lvalue afin d '"empêcher" la création de temporalités supplémentaires lors d'une opération d'addition empilée sur le type d'objet:Maintenant, d'après ce que je comprends des références rvalue, il est déconseillé de faire ce qui précède (c'est-à-dire, vous devriez simplement renvoyer une référence temporaire, pas rvalue), mais, si quelqu'un devait encore faire cela, alors vous voudriez vérifier pour faire sûr que la rvalue-reference entrante ne faisait pas référence au même objet que le
this
pointeur.la source
const
, alors vous ne pouvez lire qu'à partir de celle-ci, donc le seul besoin de faire une vérification serait si vous décidiez dans votreoperator=(const T&&)
pour effectuer la même réinitialisationthis
que vous feriez dans uneoperator=(const T&)
méthode typique plutôt qu'une opération de style swapping (c'est-à-dire, voler des pointeurs, etc. plutôt que de faire des copies complètes).Ma réponse est toujours que l'attribution de déménagement ne doit pas être sauvegardée contre l'auto-assignation, mais elle a une explication différente. Considérez std :: unique_ptr. Si je devais en implémenter un, je ferais quelque chose comme ceci:
Si vous regardez Scott Meyers expliquer cela, il fait quelque chose de similaire. (Si vous vous promenez pourquoi ne pas échanger - il a une écriture supplémentaire). Et ce n'est pas sûr pour l'auto-affectation.
Parfois c'est malheureux. Envisagez de sortir du vecteur tous les nombres pairs:
C'est correct pour les entiers mais je ne pense pas que vous puissiez faire quelque chose comme ça avec la sémantique de déplacement.
Pour conclure: déplacer l'affectation vers l'objet lui-même n'est pas correct et il faut y faire attention.
Petite mise à jour.
swap(x, x)
devrait fonctionner. Les algorithmes adorent ces choses! C'est toujours agréable quand une valise d'angle fonctionne. (Et je n'ai pas encore vu de cas où ce n'est pas gratuit. Cela ne veut pas dire que ça n'existe pas).unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...}
c'est sûr pour l'attribution de déplacement automatique.la source
Il y a une situation à laquelle (ce == rhs) je peux penser. Pour cette déclaration: Myclass obj; std :: move (obj) = std :: move (obj)
la source