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. Avait dumb_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
:
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 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 un memcpy
(caché sous std::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:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
swap(lhs, rhs);
return lhs;
}
Ou peut-être que si vous souhaitez profiter de l'affectation de déplacement en C ++ 11, cela devrait être:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
lhs = std::move(rhs);
return lhs;
}
Si dumb_array
les clients apprécient la vitesse, ils doivent appeler le operator=
. 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):
Class&
Class::operator=(Class&& rhs)
{
if (this == &rhs) // is this check needed?
{
// ...
}
return *this;
}
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:
- Un temporaire.
- Un objet que l'appelant veut vous faire croire est temporaire.
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:
Class&
Class::operator=(Class&& other)
{
assert(this != &other);
// ...
return *this;
}
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
:
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 d'utilisation typique de l'affectation de déplacement, *this
sera un objet déplacé et delete [] 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 notre dumb_array
exemple, ce sera parfaitement inoffensif si nous omettons simplement l'assertion, ou la contraignons au cas déplacé:
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 (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:
x = y;
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 de x
.
Pour l'attribution de déménagement:
x = std::move(y);
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 permettre swap(x, x)
de simplement 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, tant x = std::move(x)
qu'il ne plante pas. Il peut partir 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
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;
}
La mise en œuvre ci - dessus l' affectation automatique tolère, mais *this
et other
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.
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-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.
dumb_array& operator=(dumb_array&& other)
{
swap(other);
return *this;
}
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'il dumb_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:
Class& operator=(Class&&) = default;
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 vers strong_assign
.