C'est ma faute. (mi-blague, mi-pas).
Lorsque j'ai montré pour la première fois des exemples d'implémentations d'opérateurs d'assignation de mouvement, j'ai simplement utilisé swap. Puis un type intelligent (je ne me souviens plus qui) m'a fait remarquer que les effets secondaires de la destruction du lhs avant l'affectation pouvaient être importants (comme le unlock() dans votre exemple). J'ai donc cessé d'utiliser swap pour l'affectation des déplacements. Mais l'histoire de l'utilisation de swap est toujours là et perdure.
Il n'y a aucune raison d'utiliser swap dans cet exemple. Il est moins efficace que ce que vous suggérez. En effet, dans libc++ Je fais exactement ce que vous suggérez :
unique_lock& operator=(unique_lock&& __u)
{
if (__owns_)
__m_->unlock();
__m_ = __u.__m_;
__owns_ = __u.__owns_;
__u.__m_ = nullptr;
__u.__owns_ = false;
return *this;
}
En général, un opérateur d'affectation de mouvement devrait :
- Détruire les ressources visibles (mais peut-être sauver les ressources de détail de l'implémentation).
- Déplacer assigner toutes les bases et les membres.
- Si le déplacement des bases et des membres ne rendait pas la rhs sans ressources, alors faites-le.
Comme ça :
unique_lock& operator=(unique_lock&& __u)
{
// 1. Destroy visible resources
if (__owns_)
__m_->unlock();
// 2. Move assign all bases and members.
__m_ = __u.__m_;
__owns_ = __u.__owns_;
// 3. If the move assignment of bases and members didn't,
// make the rhs resource-less, then make it so.
__u.__m_ = nullptr;
__u.__owns_ = false;
return *this;
}
Mise à jour
Dans les commentaires, il y a une question complémentaire sur la façon de gérer les constructeurs de mouvements. J'ai commencé à y répondre (dans les commentaires), mais les contraintes de formatage et de longueur rendent difficile la création d'une réponse claire. Je mets donc ma réponse ici.
La question est la suivante : quel est le meilleur modèle pour créer un constructeur de déplacement ? Déléguer au constructeur par défaut et ensuite échanger ? Cela présente l'avantage de réduire la duplication du code.
Ma réponse est : Je pense que le point le plus important à retenir est que les programmeurs devraient se méfier de suivre des modèles sans réfléchir. Il peut y avoir des classes où l'implémentation d'un constructeur de mouvement comme default+swap est exactement la bonne réponse. La classe peut être grande et compliquée. Le site A(A&&) = default;
peut faire le mauvais choix. Je pense qu'il est important de considérer tous vos choix pour chaque classe.
Examinons en détail l'exemple de l'OP : std::unique_lock(unique_lock&&)
.
Observations :
A. Cette classe est assez simple. Elle possède deux membres de données :
mutex_type* __m_;
bool __owns_;
B. Cette classe se trouve dans une bibliothèque à usage général, destinée à être utilisée par un nombre inconnu de clients. Dans une telle situation, les préoccupations de performance sont une priorité élevée. Nous ne savons pas si nos clients vont utiliser cette classe dans un code critique pour les performances ou non. Nous devons donc supposer qu'ils le feront.
C. Le constructeur de mouvement de cette classe va consister en un petit nombre de chargements et de stockages, quoi qu'il arrive. Une bonne façon d'évaluer les performances est donc de compter les chargements et les stockages. Par exemple, si vous faites quelque chose avec 4 magasins, et que quelqu'un d'autre fait la même chose avec seulement 2 magasins, vos deux implémentations sont très rapides. Mais la leur est deux fois aussi rapide que la vôtre ! Cette différence pourrait s'avérer cruciale dans la boucle serrée de certains clients.
Tout d'abord, comptons les chargements et les stockages dans le constructeur par défaut, et dans la fonction d'échange de membres :
// 2 stores
unique_lock()
: __m_(nullptr),
__owns_(false)
{
}
// 4 stores, 4 loads
void swap(unique_lock& __u)
{
std::swap(__m_, __u.__m_);
std::swap(__owns_, __u.__owns_);
}
Implémentons maintenant le constructeur move de deux façons :
// 4 stores, 2 loads
unique_lock(unique_lock&& __u)
: __m_(__u.__m_),
__owns_(__u.__owns_)
{
__u.__m_ = nullptr;
__u.__owns_ = false;
}
// 6 stores, 4 loads
unique_lock(unique_lock&& __u)
: unique_lock()
{
swap(__u);
}
La première méthode semble beaucoup plus compliquée que la seconde. Et le code source est plus volumineux, et duplique quelque peu le code que nous avons peut-être déjà écrit ailleurs (par exemple dans l'opérateur d'affectation de déplacement). Cela signifie qu'il y a plus de risques de bogues.
La deuxième méthode est plus simple et réutilise le code que nous avons déjà écrit. Il y a donc moins de risques de bogues.
Le premier moyen est plus rapide. Si le coût des charges et des magasins est approximativement le même, peut-être 66% plus rapide !
Il s'agit d'un compromis classique d'ingénierie. Il n'y a pas de repas gratuit. Et les ingénieurs ne sont jamais libérés du fardeau d'avoir à prendre des décisions sur les compromis. Dès qu'ils le font, les avions commencent à s'écraser et les centrales nucléaires à fondre.
Pour libc++ j'ai choisi la solution la plus rapide. Mon raisonnement est le suivant : pour ce cours, j'ai intérêt à bien faire les choses quoi qu'il arrive ; le cours est suffisamment simple pour que mes chances de bien faire les choses soient élevées ; et mes clients vont apprécier la performance. Je pourrais bien arriver à une autre conclusion pour un autre cours dans un autre contexte.