Wow, il y a tellement de choses à nettoyer ici...
Tout d'abord, le Copie et échange n'est pas toujours la bonne façon d'implémenter le Copy Assignment. Presque certainement dans le cas de dumb_array
il s'agit d'une solution sous-optimale.
L'utilisation de Copie et échange est pour dumb_array
est un exemple classique de mise en place de l'opération la plus coûteuse avec les fonctionnalités les plus complètes à la couche inférieure. C'est parfait pour les clients qui veulent la fonctionnalité la plus complète et qui 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 des fonctionnalités les plus complètes et qui recherchent plutôt les performances les plus élevées. Pour eux dumb_array
est juste un autre morceau de logiciel qu'ils doivent réécrire parce qu'il est trop lent. Avait dumb_array
Si le projet avait été conçu différemment, il aurait pu satisfaire les deux clients sans qu'aucun d'eux ne soit compromis.
La clé pour satisfaire les deux clients est d'intégrer les opérations les plus rapides au niveau le plus bas, puis d'ajouter une API par-dessus pour obtenir des fonctionnalités plus complètes à un coût plus élevé. Par exemple, 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 la garantie d'exception rapide et basique de l'opérateur Copy Assignment pour dumb_array
:
dumb_array& operator=(const dumb_array& other)
{
if (this != &other)
{
if (mSize != other.mSize)
{
delete [] mArray;
mArray = nullptr;
mArray = other.mSize ? new int[other.mSize] : nullptr;
mSize = other.mSize;
}
std::copy(other.mArray, other.mArray + mSize, mArray);
}
return *this;
}
Explication :
L'une des choses les plus coûteuses que l'on puisse faire sur du matériel moderne est de faire un tour dans le tas. Tout ce que vous pouvez faire pour éviter un voyage vers le tas est du temps et des efforts bien dépensés. Les clients de dumb_array
peut vouloir assigner souvent des tableaux de la même taille. Et lorsque c'est le cas, il suffit de faire un memcpy
(caché sous std::copy
). Vous ne voulez pas allouer un nouveau tableau de la même taille et ensuite désallouer l'ancien tableau de la même taille !
Maintenant, pour vos clients qui veulent réellement une sécurité d'exception forte :
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
swap(lhs, rhs);
return lhs;
}
Ou peut-être que si vous voulez profiter de l'assignation des mouvements dans C++11, cela devrait être :
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
lhs = std::move(rhs);
return lhs;
}
Si dumb_array
Si les clients de l'entreprise accordent de l'importance à la rapidité, ils doivent faire appel à l'option operator=
. S'ils ont besoin d'une forte protection contre les exceptions, ils peuvent faire appel à 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 initiale (qui a un type-o à ce stade) :
Class&
Class::operator=(Class&& rhs)
{
if (this == &rhs) // is this check needed?
{
// ...
}
return *this;
}
Il s'agit en fait d'une question controversée. Certains diront oui, absolument, d'autres diront non.
Mon avis personnel est que non, vous n'avez pas besoin de ce contrôle.
Justification :
Lorsqu'un objet se lie à une référence rvalue, il y a deux possibilités :
- Un temporaire.
- Un objet que l'appelant veut vous faire croire qu'il est temporaire.
Si vous avez une référence à un objet qui est un temporaire réel, alors par définition, vous avez une référence unique à cet objet. Il ne peut pas être référencé par n'importe qui d'autre dans tout votre programme. C'est-à-dire que this == &temporary
n'est pas possible .
Maintenant, si votre client vous a menti et vous a promis que vous recevriez un temporaire alors que ce n'est pas le cas, alors 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 crois que ce serait une meilleure mise en œuvre :
Class&
Class::operator=(Class&& other)
{
assert(this != &other);
// ...
return *this;
}
C'est-à-dire que si vous sont passé une auto-référence, il s'agit d'un bug de la part du client qui devrait être corrigé.
Pour être complet, voici un opérateur d'assignation de déplacement pour dumb_array
:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
Dans le cas typique d'une affectation de déménagement, *this
sera un objet déplacé et donc delete [] mArray;
ne doit pas être une option. Il est essentiel que les implémentations rendent la suppression d'un nullptr aussi rapide que possible.
Attention :
Certains diront que swap(x, x)
est une bonne idée, ou juste un mal nécessaire. Et ceci, si le swap va vers le swap par défaut, peut provoquer un auto-mouvement d'affectation.
Je ne suis pas d'accord avec le fait que swap(x, x)
es jamais une bonne idée. Si je le trouve dans mon propre code, je le considérerai comme un bug de performance et je le corrigerai. Mais si vous voulez l'autoriser, sachez que swap(x, x)
ne fait que de l'auto-mouvement-assignation sur une valeur déplacée. Et dans notre dumb_array
Par exemple, cela sera parfaitement inoffensif si nous omettons simplement l'assertion, ou si nous la limitons au cas de déplacement de l'objet :
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other || mSize == 0);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
Si vous attribuez vous-même deux déplacés de (vide) dumb_array
vous ne faites rien d'incorrect, à part insérer des instructions inutiles dans votre programme. Cette même observation peut être faite pour la grande majorité des objets.
<
Mise à jour >
J'ai réfléchi à cette question et j'ai quelque peu modifié ma position. Je pense maintenant que l'assignation doit être tolérée pour l'auto-assignation, mais que les conditions d'affectation sur l'assignation de copie et l'assignation de déplacement sont différentes :
Pour l'attribution des copies :
x = y;
on devrait avoir une post-condition que la valeur de y
ne doivent pas être modifiées. Lorsque &x == &y
alors cette postcondition se traduit par : l'affectation de la copie de soi ne doit avoir aucun impact sur la valeur de x
.
Pour l'affectation des mouvements :
x = std::move(y);
on devrait avoir une post-condition qui y
a un état valide mais non spécifié. Lorsque &x == &y
alors cette postcondition se traduit par : x
a un état valide mais non spécifié. C'est-à-dire que l'assignation du mouvement personnel ne doit pas nécessairement être un no-op. Mais il ne doit pas se planter. Cette post-condition est cohérente avec l'autorisation de swap(x, x)
pour travailler :
template <class T>
void
swap(T& x, T& y)
{
// assume &x == &y
T tmp(std::move(x));
// x and y now have a valid but unspecified state
x = std::move(y);
// x and y still have a valid but unspecified state
y = std::move(tmp);
// x and y have the value of tmp, which is the value they had on entry
}
Ce qui précède fonctionne, à condition que x = std::move(x)
ne s'écrase pas. Il peut laisser x
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
pour y parvenir :
dumb_array& operator=(dumb_array&& other)
{
delete [] mArray;
// set *this to a valid state before continuing
mSize = 0;
mArray = nullptr;
// *this is now in a valid state, continue with move assignment
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
L'implémentation ci-dessus tolère l'auto-assignation, mais *this
y other
finissent par être un tableau de taille nulle après l'affectation d'auto-déplacement, quelle que soit la valeur initiale de *this
est. C'est bien.
dumb_array& operator=(dumb_array&& other)
{
if (this != &other)
{
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
}
return *this;
}
L'implémentation ci-dessus tolère l'auto-assignation de la même manière que l'opérateur d'affectation par copie, en en faisant un no-op. C'est également très bien.
dumb_array& operator=(dumb_array&& other)
{
swap(other);
return *this;
}
Ce qui précède n'est acceptable que si 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 est parfait. Si dumb_array
pourrait éventuellement contenir des verrous mutex ou l'état d'ouverture de fichiers, le client pourrait raisonnablement s'attendre à ce que ces ressources sur le côté gauche de l'affectation de déplacement soient immédiatement libérées et donc cette mise en œuvre pourrait être problématique.
Le coût du premier est de deux magasins supplémentaires. Le coût du second est un test-and-branch. Les deux fonctionnent. Les deux répondent à toutes les exigences du tableau 22 MoveAssignable de la norme C++11. La troisième fonctionne également modulo la préoccupation de la non-ressource mémoire.
Ces trois implémentations peuvent avoir des coûts différents en fonction du matériel : quel est le coût d'un branchement ? Y a-t-il beaucoup de registres ou très peu ?
Ce qu'il faut retenir, c'est que l'affectation par déplacement automatique, contrairement à l'affectation par copie automatique, ne doit pas nécessairement préserver la valeur actuelle.
<
/Mise à jour >
Une dernière modification (je l'espère) inspirée par le commentaire de Luc Danton :
Si vous écrivez une classe de haut niveau qui ne gère pas directement la mémoire (mais qui peut avoir des bases ou des membres qui le font), alors la meilleure implémentation de l'affectation des déplacements est souvent :
Class& operator=(Class&&) = default;
Ce mouvement affectera chaque base et chaque membre à tour de rôle, et ne comprendra pas de this != &other
vérifier. Vous obtiendrez ainsi les meilleures performances et une sécurité de base contre les exceptions, en supposant qu'aucune invariante ne doive être maintenue parmi vos bases et vos membres. Pour vos clients exigeant une forte protection contre les exceptions, dirigez-les vers strong_assign
.
12 votes
Sans rapport avec la question posée, et juste pour que les nouveaux utilisateurs qui lisent cette question dans le futur (car je sais que Seth le sait déjà) ne se fassent pas de fausses idées, Copie et échange est la manière correcte d'implémenter l'opérateur d'assignation de copie dans lequel vous n'avez pas besoin de vérifier l'auto-assignation et-all.
0 votes
La signature doit-elle être
const Class&&
oClass&&
?0 votes
Dans des circonstances normales, l'affectation de mouvements vers/depuis le même objet ne pourrait pas se produire. La référence rvalue signifie que vous effectuez une affectation à partir d'un objet temporaire et que rien d'autre ne devrait y faire référence. Une assertion serait peut-être plus appropriée.
5 votes
@VaughnCato :
A a; a = std::move(a);
.0 votes
@Xeo : d'accord -- je ne considérerais pas cela comme normal cependant.
11 votes
@VaughnCato en utilisant
std::move
est normal. Ensuite, il faut tenir compte de l'aliasing, et lorsque vous êtes au fond d'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 même ? Voulez-vous trouver le premier appel (ou les premiers appels) où le fait de documenter que vous ne pouvez pas passer le même argument deux fois prouvera statiquement que ces deux références ne s'aliaseront pas ? Ou bien allez-vous faire en sorte que l'auto-affectation fonctionne tout simplement ?2 votes
@LucDanton Je préférerais une assertion dans l'opérateur d'affectation. Si std::move était utilisé de telle manière qu'il était possible de se retrouver avec une auto-assignation de rvalue, je considérerais cela comme un bug qui devrait être corrigé.
4 votes
Un endroit où l'échange automatique est normal est à l'intérieur de l'un ou l'autre.
std::sort
ostd::shuffle
- à chaque fois que vous échangez lei
etj
d'un tableau sans vérifieri != j
premier. (std::swap
est mis en œuvre en termes d'affectation des déplacements).