Comment mettre en œuvre la règle des cinq?

MISE À JOUR en bas

q1: Comment implémenteriez-vous la règle des cinq pour une classe qui gère des ressources plutôt lourdes, mais dont vous voulez qu’elle soit transmise par valeur, car cela simplifie grandement son utilisation? Ou les cinq éléments de la règle ne sont-ils pas tous nécessaires?

En pratique, je commence quelque chose avec l’imagerie 3D où une image est généralement de 128 * 128 * 128 doubles. Pouvoir écrire des choses comme ça rendrait les calculs beaucoup plus faciles:

Data a = MakeData(); Data c = 5 * a + ( 1 + MakeMoreData() ) / 3; 

q2: En utilisant une combinaison de sémantique de copie elision / RVO / move, le compilateur devrait pouvoir le faire avec un minimum de copie, non?

J’ai essayé de comprendre comment faire, alors j’ai commencé avec les bases; supposons un object implémentant la méthode traditionnelle d’implémentation de la copie et de l’affectation:

 class AnObject { public: AnObject( size_t n = 0 ) : n( n ), a( new int[ n ] ) {} AnObject( const AnObject& rh ) : n( rh.n ), a( new int[ rh.n ] ) { std::copy( rh.a, rh.a + n, a ); } AnObject& operator = ( AnObject rh ) { swap( *this, rh ); return *this; } friend void swap( AnObject& first, AnObject& second ) { std::swap( first.n, second.n ); std::swap( first.a, second.a ); } ~AnObject() { delete [] a; } private: size_t n; int* a; }; 

Entrez maintenant les valeurs et déplacez la sémantique. Autant que je sache, cela serait une implémentation de travail:

 AnObject( AnObject&& rh ) : n( rh.n ), a( rh.a ) { rh.n = 0; rh.a = nullptr; } AnObject& operator = ( AnObject&& rh ) { n = rh.n; a = rh.a; rh.n = 0; rh.a = nullptr; return *this; } 

Cependant, le compilateur (VC ++ 2010 SP1) n’est pas très satisfait de cela et les compilateurs sont généralement corrects:

 AnObject make() { return AnObject(); } int main() { AnObject a; a = make(); //error C2593: 'operator =' is ambiguous } 

q3: comment résoudre ce problème? Revenir à AnObject & operator = (const AnObject & rh) le corrige certainement, mais ne perdons-nous pas une opportunité d’optimisation assez importante?

En dehors de cela, il est clair que le code pour le constructeur et l’affectation de déplacement est plein de duplication. Donc pour l’instant nous oublions l’ambiguïté et essayons de résoudre ceci en utilisant la copie et l’échange mais maintenant pour les valeurs. Comme expliqué ici, nous n’aurions même pas besoin d’un swap personnalisé, mais plutôt std :: swap fait tout le travail, ce qui semble très prometteur. J’ai donc écrit ce qui suit, en espérant que std :: swap copierait la construction d’un temporaire en utilisant le constructeur move, puis le remplacerait par * this:

 AnObject& operator = ( AnObject&& rh ) { std::swap( *this, rh ); return *this; } 

Mais cela ne fonctionne pas et mène plutôt à un débordement de stack dû à une récursion infinie puisque std :: swap appelle notre opérateur = (AnObject && rh) à nouveau. q4: Quelqu’un peut-il donner un exemple de ce que signifie l’exemple alors?

Nous pouvons résoudre ce problème en fournissant une deuxième fonction d’échange:

 AnObject( AnObject&& rh ) { swap( *this, std::move( rh ) ); } AnObject& operator = ( AnObject&& rh ) { swap( *this, std::move( rh ) ); return *this; } friend void swap( AnObject& first, AnObject&& second ) { first.n = second.n; first.a = second.a; second.n = 0; second.a = nullptr; } 

Maintenant, il y a presque deux fois le code de la quantité, mais la partie en vaut la peine en permettant des déménagements bon marché; mais d’autre part, l’atsortingbution normale ne peut plus bénéficier de l’élision de la copie. À ce stade, je suis vraiment confus, et ne plus voir ce qui est bien et ce qui est faux, alors j’espère avoir des idées ici.

MISE À JOUR Il semble donc qu’il y ait deux camps:

  • l’un d’eux dit de sauter l’opérateur d’affectation de déplacement et de continuer à faire ce que C ++ 03 nous a appris, c’est-à-dire écrire un seul opérateur d’affectation qui transmet l’argument par valeur.
  • l’autre qui dit d’implémenter l’opérateur d’affectation de mouvement (après tout, c’est C ++ 11) et qui demande à l’opérateur d’atsortingbution de copie de prendre son argument par référence.

(ok et il y a le 3ème camp qui me dit d’utiliser un vecteur, mais c’est un peu hors de propos pour cette classe hypothétique. Ok dans la vraie vie, j’utiliserais un vecteur, et il y aurait aussi d’autres membres, mais depuis le constructeur l’affectation n’est pas générée automatiquement (encore?), la question est toujours d’actualité)

Malheureusement, je ne peux pas tester les deux implémentations dans un scénario réel puisque ce projet vient juste de démarrer et que la manière dont les données vont réellement circuler n’est pas encore connue. Donc, j’ai simplement implémenté les deux, ajouté des compteurs pour l’allocation, etc. et j’ai effectué quelques itérations d’env. ce code, où T est l’une des implémentations:

 template T make() { return T( narraySize ); } template void assign( T& r ) { r = make(); } template void Test() { T a; T b; for( size_t i = 0 ; i < numIter ; ++i ) { assign( a ); assign( b ); T d( a ); T e( b ); T f( make() ); T g( make() + make() ); } } 

Soit ce code n’est pas assez bon pour tester ce que je recherche, soit le compilateur est tout simplement trop intelligent: peu importe ce que j’utilise pour arraySize et numIter, les résultats pour les deux camps sont quasiment identiques: même nombre d’allocations, très légères variations de timing mais pas de différence significative reproductible.

Donc, à moins que quelqu’un ne puisse trouver un meilleur moyen de tester cela (étant donné que les scnearios d’utilisation ne sont pas encore connus), je devrai en conclure que cela n’a pas d’importance et est donc laissé au goût du développeur. Dans ce cas, je choisirais # 2.

Vous avez manqué une optimisation significative dans votre opérateur d’affectation de copie. Et par la suite, la situation est devenue confuse.

  AnObject& operator = ( const AnObject& rh ) { if (this != &rh) { if (n != rh.n) { delete [] a; n = 0; a = new int [ rh.n ]; n = rh.n; } std::copy(rh.a, rh.a+n, a); } return *this; } 

A moins que vous ne pensiez vraiment que vous n’atsortingbuez AnObject de même taille, c’est bien mieux. Ne jetez jamais de ressources si vous pouvez les recycler.

Certains pourraient se plaindre du fait que l’opérateur d’atsortingbution de copies d’ AnObject ne dispose désormais que d’une sécurité d’exception de base au lieu d’une sécurité exceptionnelle contre les exceptions. Cependant, considérez ceci:

Vos clients peuvent toujours prendre un opérateur d’affectation rapide et lui donner une forte sécurité d’exception. Mais ils ne peuvent pas prendre un opérateur d’affectation lente et le rendre plus rapide.

 template  T& strong_assign(T& x, T y) { swap(x, y); return x; } 

Votre constructeur de déplacement est correct, mais votre opérateur d’affectation de déplacement a une fuite de mémoire. CA devrait etre:

  AnObject& operator = ( AnObject&& rh ) { delete [] a; n = rh.n; a = rh.a; rh.n = 0; rh.a = nullptr; return *this; } 

 Data a = MakeData(); Data c = 5 * a + ( 1 + MakeMoreData() ) / 3; 

q2: En utilisant une combinaison de sémantique de copie elision / RVO / move, le compilateur devrait pouvoir le faire avec un minimum de copie, non?

Vous devrez peut-être surcharger vos opérateurs pour tirer parti des ressources disponibles dans les valeurs:

 Data operator+(Data&& x, const Data& y) { // recycle resources in x! x += y; return std::move(x); } 

En fin de compte, les ressources doivent être créées une seule fois pour chaque Data vous intéresse. Il ne devrait y avoir aucune new/delete inutile dans le seul but de déplacer les choses.

Si votre object est lourd en ressources, vous voudrez peut-être éviter de le copier complètement et fournir simplement le constructeur de déplacement et l’opérateur de déplacement. Cependant, si vous voulez vraiment copier, il est facile de fournir toutes les opérations.

Vos opérations de copie semblent judicieuses, mais pas vos opérations de déplacement. Tout d’abord, bien qu’un paramètre de référence rvalue se liera à une valeur, dans la fonction, c’est une valeur lvalue , donc votre constructeur de déplacement doit être:

 AnObject( AnObject&& rh ) : n( std::move(rh.n) ), a( std::move(rh.a) ) { rh.n = 0; rh.a = nullptr; } 

Bien sûr, pour les types fondamentaux comme ceux que vous avez ici, cela ne fait pas vraiment de différence, mais il faut aussi prendre l’habitude.

Si vous fournissez un constructeur de déplacement, vous n’avez pas besoin d’opérateur d’assignation de mouvement lorsque vous définissez une affectation de copie comme vous l’avez fait — car vous acceptez le paramètre par valeur , une valeur sera déplacée dans le paramètre plutôt que copiée .

Comme vous l’avez constaté, vous ne pouvez pas utiliser std::swap() sur l’object entier à l’intérieur d’un opérateur d’affectation de mouvement, car celui-ci sera renvoyé dans l’opérateur d’affectation de mouvement. Le point du commentaire dans la publication à laquelle vous êtes lié est que vous n’avez pas besoin d’implémenter un swap personnalisé si vous fournissez des opérations de déplacement, car std::swap utilisera vos opérations de déplacement. Malheureusement, si vous ne définissez pas d’opérateur d’affectation de mouvement distinct, cela ne fonctionne pas et restra en attente. Vous pouvez bien sûr utiliser std::swap pour échanger les membres:

 AnObject& operator=(AnObject other) { std::swap(n,other.n); std::swap(a,other.a); return *this; } 

Votre dernière classe est donc:

 class AnObject { public: AnObject( size_t n = 0 ) : n( n ), a( new int[ n ] ) {} AnObject( const AnObject& rh ) : n( rh.n ), a( new int[ rh.n ] ) { std::copy( rh.a, rh.a + n, a ); } AnObject( AnObject&& rh ) : n( std::move(rh.n) ), a( std::move(rh.a) ) { rh.n = 0; rh.a = nullptr; } AnObject& operator = ( AnObject rh ) { std::swap(n,rh.n); std::swap(a,rh.a); return *this; } ~AnObject() { delete [] a; } private: size_t n; int* a; }; 

Laissez-moi vous aider:

 #include  class AnObject { public: AnObject( size_t n = 0 ) : data(n) {} private: std::vector data; }; 

A partir de C ++ 0x FDIS, [class.copy] note 9:

Si la définition d’une classe X ne déclare pas explicitement un constructeur de déplacement, on sera implicitement déclaré comme étant par défaut si et seulement si

  • X n’a ​​pas de constructeur de copie déclaré par l’utilisateur,

  • X n’a ​​pas d’opérateur d’affectation de copie déclaré par l’utilisateur,

  • X n’a ​​pas d’opérateur d’affectation de mouvement déclaré par l’utilisateur,

  • X n’a ​​pas de destructeur déclaré par l’utilisateur, et

  • le constructeur de déplacement ne serait pas implicitement défini comme supprimé.

[Remarque: Lorsque le constructeur de déplacement n’est pas déclaré ou fourni de manière implicite, les expressions qui auraient autrement appelé le constructeur de déplacement peuvent à la place appeler un constructeur de copie. —End note]

Personnellement, je suis beaucoup plus confiant dans std::vector gérant correctement ses ressources et optimisant les copies / déplacements que tout code que je pourrais écrire.

Comme je n’ai vu personne d’autre le signaler explicitement …

Votre opérateur d’affectation de copie prenant son argument par valeur est une opportunité d’optimisation importante si (et seulement si) il a reçu une valeur, en raison d’une élision de copie. Mais dans une classe avec un opérateur d’affectation qui ne prend explicitement que des valeurs (c.-à-d. Une avec un opérateur d’affectation de mouvement), il s’agit d’un scénario absurde. Donc, modulo les memory leaks qui ont déjà été signalées dans d’autres réponses, je dirais que votre classe est déjà idéale si vous changez simplement l’opérateur d’affectation de copie pour prendre son argument par référence const.

q3 de l’affiche originale

Je pense que vous (et certains des autres intervenants) avez mal compris ce que signifiait l’erreur du compilateur et que vous en avez tiré des conclusions erronées. Le compilateur pense que l’appel d’affectation (déplacer) est ambigu, et c’est vrai! Vous avez plusieurs méthodes également qualifiées.

Dans votre version d’origine de la classe AnObject , votre constructeur de copie prend l’ancien object par référence const (lvalue), tandis que l’opérateur d’affectation prend son argument par valeur (non qualifiée). L’argument value est initialisé par le constructeur de transfert approprié, quel que soit le côté droit de l’opérateur. Comme vous n’avez qu’un seul constructeur de transfert, ce constructeur de copie est toujours utilisé, peu importe si l’expression d’origine à droite était une lvalue ou une rvalue. Cela permet à l’opérateur d’affectation d’agir en tant que fonction de membre spécial d’affectation de copie.

La situation change une fois qu’un constructeur de déplacement est ajouté. Chaque fois que l’opérateur d’affectation est appelé, il existe deux choix pour le constructeur de transfert. Le constructeur de copie sera toujours utilisé pour les expressions lvalue, mais le constructeur move sera utilisé chaque fois qu’une expression rvalue est donnée à la place! Cela permet à l’opérateur d’affectation d’agir simultanément comme fonction de membre spécial d’affectation de déplacement.

Lorsque vous avez ajouté un opérateur d’assignation de déplacement traditionnel, vous avez atsortingbué à la classe deux versions de la même fonction de membre spéciale, ce qui constitue une erreur. Vous avez déjà ce que vous vouliez, alors débarrassez-vous de l’opérateur d’affectation de mouvement traditionnel et aucun autre changement ne devrait être nécessaire.

Dans les deux camps énumérés dans votre mise à jour, je pense que je suis techniquement dans le premier camp, mais pour des raisons totalement différentes. (Ne sautez pas l’opérateur (traditionnel) d’atsortingbution de mouvement car il est “cassé” pour votre classe, mais parce que c’est superflu.)

BTW, je suis nouveau à la lecture de C ++ 11 et StackOverflow. J’ai trouvé cette réponse en parcourant une autre question SO avant de voir celle-ci. ( Mise à jour : en fait, j’avais toujours la page ouverte. Le lien renvoie à la réponse spécifique de FredOverflow qui montre la technique.)

À propos de la réponse du 12 mai 2011 de Howard Hinnant

(Je suis trop novice pour commenter directement les réponses.)

Vous n’avez pas besoin de vérifier explicitement l’auto-affectation si un test ultérieur le résout déjà. Dans ce cas, n != rh.n occuperait déjà. Cependant, l’appel std::copy est en dehors de ce (actuellement) internal if , nous aurions donc n auto-assignations au niveau des composants. C’est à vous de décider si ces missions seraient trop anti-optimales même si l’auto-affectation devrait être rare.

Avec l’aide du constructeur délégué, il vous suffit d’implémenter chaque concept une seule fois;

  • init par défaut
  • suppression de ressource
  • échanger
  • copie

le rest n’utilise que ceux-là.

N’oubliez pas non plus de faire une affectation par déplacement (et un échange) noexcept , si cela aide beaucoup les performances si, par exemple, vous placez votre classe dans un vector

 #include  // header class T { public: T(); T(const T&); T(T&&); T& operator=(const T&); T& operator=(T&&) noexcept; ~T(); void swap(T&) noexcept; private: void copy_from(const T&); }; // implementation T::T() { // here (and only here) you implement default init } T::~T() { // here (and only here) you implement resource delete } void T::swap(T&) noexcept { using std::swap; // enable ADL // here (and only here) you implement swap, typically memberwise swap } void T::copy_from(const T& t) { if( this == &t ) return; // don't forget to protect against self assign // here (and only here) you implement copy } // the rest is generic: T::T(const T& t) : T() { copy_from(t); } T::T(T&& t) : T() { swap(t); } auto T::operator=(const T& t) -> T& { copy_from(t); return *this; } auto T::operator=(T&& t) noexcept -> T& { swap(t); return *this; }