Quand faut-il utiliser des constructeurs de copie?

Je sais que le compilateur C ++ crée un constructeur de copie pour une classe. Dans quel cas devons-nous écrire un constructeur de copie défini par l’utilisateur? Peux-tu donner quelques exemples?

Le constructeur de copie généré par le compilateur effectue une copie par membre. Parfois, ce n’est pas suffisant. Par exemple:

class Class { public: Class( const char* str ); ~Class(); private: char* stored; }; Class::Class( const char* str ) { stored = new char[srtlen( str ) + 1 ]; strcpy( stored, str ); } Class::~Class() { delete[] stored; } 

dans ce cas, la copie membre du membre stored ne dupliquera pas le tampon (seul le pointeur sera copié), de sorte que le premier partage de copie à détruire le tampon appellera delete[] avec succès et le second à un comportement indéfini. Vous avez besoin de copier en profondeur le constructeur de copie (ainsi que l’opérateur d’affectation).

 Class::Class( const Class& another ) { stored = new char[strlen(another.stored) + 1]; strcpy( stored, another.stored ); } void Class::operator = ( const Class& another ) { char* temp = new char[strlen(another.stored) + 1]; strcpy( temp, another.stored); delete[] stored; stored = temp; } 

Je suis un peu ennuyé que la règle de la Rule of Five n’ait pas été citée.

Cette règle est très simple:

La règle des cinq :
Chaque fois que vous écrivez soit Destructor, Copy Constructor, Copy Assignment Operator, Move Construct ou Move Assignment Operator, vous devez probablement écrire les quatre autres.

Mais il y a une directive plus générale que vous devriez suivre, qui découle de la nécessité d’écrire du code sécurisé d’exception:

Chaque ressource doit être gérée par un object dédié

Ici, le code de @sharptooth est toujours (surtout) @sharptooth , mais s’il ajoutait un deuxième atsortingbut à sa classe, ce ne serait pas le cas. Considérons la classe suivante:

 class Erroneous { public: Erroneous(); // ... others private: Foo* mFoo; Bar* mBar; }; Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {} 

Que se passe-t-il si une new Bar lance? Comment supprimez-vous l’object pointé par mFoo ? Il y a des solutions (essayer / rattraper au niveau de la fonction …), elles ne sont tout simplement pas à l’échelle.

La manière appropriée de gérer la situation est d’utiliser des classes appropriées au lieu de pointeurs bruts.

 class Righteous { public: private: std::unique_ptr mFoo; std::unique_ptr mBar; }; 

Avec la même implémentation du constructeur (ou en fait, en utilisant make_unique ), j’ai maintenant la sécurité des exceptions gratuitement !!! N’est-ce pas excitant? Et le meilleur de tous, je n’ai plus besoin de m’inquiéter d’un destructeur approprié! Je dois cependant écrire mon propre Copy Constructor et mon propre Assignment Operator , car unique_ptr ne définit pas ces opérations … mais cela n’a pas d’importance ici;)

Et donc, la classe de sharptooth ‘s revisitée:

 class Class { public: Class(char const* str): mData(str) {} private: std::ssortingng mData; }; 

Je ne sais pas pour vous, mais je trouve le mien plus facile;)

Je peux me rappeler de ma pratique et penser aux cas suivants où il faut traiter explicitement de déclarer / définir le constructeur de la copie. J’ai regroupé les cas en deux catégories

  • Correct / Sémantique – Si vous ne fournissez pas de constructeur de copie défini par l’utilisateur, les programmes utilisant ce type risquent de ne pas pouvoir être compilés ou fonctionneront incorrectement.
  • Optimisation – fournir une bonne alternative au constructeur de copie généré par le compilateur permet de rendre le programme plus rapide.


Correct / Sémantique

Je place dans cette section les cas où la déclaration / définition du constructeur de la copie est nécessaire au bon fonctionnement des programmes utilisant ce type.

Après avoir lu cette section, vous découvrirez plusieurs écueils de permettre au compilateur de générer lui-même le constructeur de copie. Par conséquent, comme il a été noté dans sa réponse , il est toujours prudent de désactiver la copie pour une nouvelle classe et de l’ activer délibérément plus tard, lorsque cela est vraiment nécessaire.

Comment rendre une classe non copiable en C ++ 03

Déclarez un constructeur de copie privé et ne l’implémentez pas (pour que la construction échoue à l’étape de liaison même si les objects de ce type sont copiés dans la scope de la classe ou par ses amis).

Comment rendre une classe non copiable en C ++ 11 ou plus récent

Déclarez le constructeur de copie avec =delete à la fin.


Copie superficielle vs profonde

C’est le cas le mieux compris et le seul mentionné dans les autres réponses. shaprtooth l’ a bien couvert . Je veux seulement append que la copie en profondeur de ressources qui devraient appartenir exclusivement à l’object peut s’appliquer à tout type de ressources, dont la mémoire allouée dynamicment n’est qu’un type. Si nécessaire, une copie en profondeur d’un object peut également nécessiter

  • copier des fichiers temporaires sur le disque
  • ouverture d’une connexion réseau séparée
  • créer un thread de travail distinct
  • allouer un framebuffer OpenGL séparé
  • etc

Objets à enregistrement automatique

Considérons une classe où tous les objects – peu importe la manière dont ils ont été construits – DOIVENT être enregistrés d’une manière ou d’une autre. Quelques exemples:

  • L’exemple le plus simple: conserver le nombre total d’objects existants. L’enregistrement d’object consiste simplement à incrémenter le compteur statique.

  • Un exemple plus complexe est d’avoir un registre singleton, où les références à tous les objects existants de ce type sont stockées (afin que les notifications puissent être envoyées à tous).

  • Les pointeurs de référence comptés peuvent être considérés comme un cas particulier dans cette catégorie: le nouveau pointeur “s’enregistre” lui-même avec la ressource partagée plutôt que dans un registre global.

Une telle opération d’auto-enregistrement doit être effectuée par n’importe quel constructeur du type et le constructeur de copie ne fait pas exception.


Objets avec références croisées internes

Certains objects peuvent avoir une structure interne non sortingviale avec des références croisées directes entre leurs différents sous-objects (en fait, une seule référence croisée interne suffit pour déclencher ce cas). Le constructeur de copie fourni par le compilateur va briser les associations intra-objects internes, en les convertissant en associations inter-objects .

Un exemple:

 struct MarriedMan; struct MarriedWoman; struct MarriedMan { // ... MarriedWoman* wife; // association }; struct MarriedWoman { // ... MarriedMan* husband; // association }; struct MarriedCouple { MarriedWoman wife; // aggregation MarriedMan husband; // aggregation MarriedCouple() { wife.husband = &husband; husband.wife = &wife; } }; MarriedCouple couple1; // couple1.wife and couple1.husband are spouses MarriedCouple couple2(couple1); // Are couple2.wife and couple2.husband indeed spouses? // Why does couple2.wife say that she is married to couple1.husband? // Why does couple2.husband say that he is married to couple1.wife? 

Seuls les objects répondant à certains critères peuvent être copiés

Il peut y avoir des classes où les objects peuvent être copiés en toute sécurité dans un état (par exemple, état construit par défaut) et ne pas être sûrs de copier autrement. Si nous voulons autoriser la copie d’objects sûrs à copier, alors, si la programmation est défensive, nous avons besoin d’une vérification au moment de l’exécution dans le constructeur de copie défini par l’utilisateur.


Sous-objects non copiables

Parfois, une classe qui doit être copiable agrège les sous-objects non copiables. Habituellement, cela se produit pour les objects avec un état non observable (ce cas est discuté plus en détail dans la section “Optimisation” ci-dessous). Le compilateur aide simplement à reconnaître ce cas.


Sous-objects quasi-copiables

Une classe, qui doit être copiable, peut agréger un sous-object de type quasi-copiable. Un type quasi-copiable ne fournit pas de constructeur de copie au sens ssortingct, mais un autre constructeur permet de créer une copie conceptuelle de l’object. La raison pour laquelle un type est quasi-copiable, c’est quand il n’y a pas d’accord complet sur la sémantique de la copie du type.

Par exemple, en revisitant le cas d’auto-enregistrement de l’object, on peut affirmer qu’il peut y avoir des situations où un object doit être enregistré avec le gestionnaire d’objects global uniquement s’il s’agit d’un object autonome complet. S’il s’agit d’un sous-object d’un autre object, la responsabilité de la gérer est avec son object contenant.

Ou bien, la copie superficielle et profonde doit être prise en charge (aucune d’entre elles n’étant la copie par défaut).

Ensuite, la décision finale est laissée aux utilisateurs de ce type – lors de la copie d’objects, ils doivent explicitement spécifier (via des arguments supplémentaires) la méthode de copie prévue.

Dans le cas d’une approche non défensive de la programmation, il est également possible qu’un constructeur de copie régulier et un constructeur de quasi-copie soient présents. Cela peut se justifier lorsque, dans la grande majorité des cas, une méthode de copie unique doit être appliquée, alors que dans des situations rares mais bien comsockets, des méthodes de copie alternatives doivent être utilisées. Ensuite, le compilateur ne se plaindra pas de l’impossibilité de définir implicitement le constructeur de la copie. Il appartient à l’utilisateur de se rappeler et de vérifier si un sous-object de ce type doit être copié via un constructeur de quasi-copie.


Ne copiez pas l’état fortement associé à l’identité de l’object

Dans de rares cas, un sous-ensemble de l’état observable de l’object peut constituer (ou être considéré) une partie indissociable de l’identité de l’object et ne doit pas être transférable à d’autres objects (bien que cela puisse être quelque peu controversé).

Exemples:

  • L’ID utilisateur de l’object (mais celui-ci appartient également au cas “d’auto-enregistrement” ci-dessus, car l’identifiant doit être obtenu lors d’un acte d’auto-enregistrement).

  • Historique de l’object (par exemple, stack Undo / Redo) dans le cas où le nouvel object ne doit pas hériter de l’historique de l’object source, mais plutôt avec un seul élément de l’historique ” Copié à “.

Dans de tels cas, le constructeur de copie doit ignorer la copie des sous-objects correspondants.


Appliquer la signature correcte du constructeur de copie

La signature du constructeur de copie fourni par le compilateur dépend des constructeurs de copie disponibles pour les sous-objects. Si au moins un sous-object n’a pas de constructeur de copie réel (prenant l’object source par référence constante) mais un constructeur de copie mutant (prenant l’object source par référence non constante), le compilateur n’aura pas le choix mais de déclarer et de définir implicitement un constructeur de copie en mutation.

Maintenant, que se passe-t-il si le constructeur de copie “mutant” du type du sous-object ne modifie pas réellement l’object source (et a simplement été écrit par un programmeur qui ne connaît pas le mot-clé const )? Si nous ne pouvons pas corriger ce code en ajoutant le const manquant, l’autre option consiste à déclarer notre propre constructeur de copie défini par l’utilisateur avec une signature correcte et à commettre le péché de const_cast .


Copie sur écriture (COW)

Un conteneur COW qui a donné des références directes à ses données internes DOIT être copié en profondeur au moment de la construction, sinon il peut se comporter comme une poignée de comptage de référence.

Bien que COW soit une technique d’optimisation, cette logique dans le constructeur de copie est cruciale pour sa mise en œuvre correcte. C’est pourquoi j’ai placé ce cas ici plutôt que dans la section “Optimisation”, où nous allons ensuite.



Optimisation

Dans les cas suivants, vous souhaiterez peut-être / définir votre propre constructeur de copie à partir des problèmes d’optimisation:


Optimisation de la structure pendant la copie

Pensez à un conteneur qui prend en charge les opérations de suppression d’éléments, mais vous pouvez le faire simplement en marquant l’élément supprimé comme supprimé et en recyclant son emplacement plus tard. Lorsqu’une copie d’un tel conteneur est faite, il peut être judicieux de compacter les données restantes plutôt que de conserver les emplacements “supprimés” tels quels.


Ignorer la copie d’état non observable

Un object peut contenir des données qui ne font pas partie de son état observable. Habituellement, il s’agit de données mises en cache / mémorisées accumulées au cours de la durée de vie de l’object afin d’accélérer certaines opérations de requête lentes effectuées par l’object. Il est prudent de ne pas copier ces données car elles seront recalculées lorsque (et si!) Les opérations correspondantes sont effectuées. La copie de ces données peut être injustifiée, car elle peut être rapidement invalidée si l’état observable de l’object (à partir duquel les données en cache sont dérivées) est modifié par des opérations de mutation (et si nous ne modifions pas l’object, copie alors?)

Cette optimisation n’est justifiée que si les données auxiliaires sont importantes par rapport aux données représentant l’état observable.


Désactiver la copie implicite

C ++ permet de désactiver la copie implicite en déclarant le constructeur de la copie explicit . Ensuite, les objects de cette classe ne peuvent pas être transmis à des fonctions et / ou renvoyés à partir de fonctions par valeur. Cette astuce peut être utilisée pour un type qui semble léger mais qui est en effet très coûteux à copier (bien que le rendre quasi-copiable puisse être un meilleur choix).

En C ++ 03, la déclaration d’un constructeur de copie nécessitait également de la définir (bien sûr, si vous aviez l’intention de l’utiliser). Par conséquent, le simple fait de rechercher un tel constructeur de copie simplement à partir du problème en question signifiait que vous deviez écrire le même code que le compilateur générerait automatiquement pour vous.

Les normes C ++ 11 et ultérieures permettent de déclarer des fonctions membres spéciales (constructeurs par défaut et copie, opérateur d’atsortingbution de copie et destructeur) avec une demande explicite d’utilisation de l’implémentation par défaut (il suffit de terminer la déclaration avec =default ).



TODOs

Cette réponse peut être améliorée comme suit:

  • Ajouter plus d’exemple de code
  • Illustrer le cas “Objets avec références croisées internes”
  • Ajouter des liens

Si vous avez une classe qui a alloué dynamicment du contenu. Par exemple, vous stockez le titre d’un livre en tant que caractère * et définissez le titre avec nouveau, la copie ne fonctionnera pas.

Vous devrez écrire un constructeur de copie qui fait title = new char[length+1] , puis strcpy(title, titleIn) . Le constructeur de la copie ne ferait qu’une copie “superficielle”.

Le constructeur de copie est appelé lorsqu’un object est transmis par valeur, renvoyé par valeur ou explicitement copié. S’il n’y a pas de constructeur de copie, c ++ crée un constructeur de copie par défaut qui crée une copie superficielle. Si l’object n’a pas de pointeur vers la mémoire allouée dynamicment, alors la copie superficielle fera l’affaire.

C’est souvent une bonne idée de désactiver copy ctor et operator = à moins que la classe n’en ait spécifiquement besoin. Cela peut éviter les inefficacités telles que la transmission d’un argument par valeur lorsque la référence est prévue. De plus, les méthodes générées par le compilateur peuvent être invalides.