Quand devons-nous utiliser les constructeurs de copies?

je sais que le compilateur C++ crée un constructeur de copie pour une classe. Dans quel cas faut-il écrire un constructeur de copie? Pouvez-vous donner quelques exemples?

72
demandé sur harshvchawla 2010-07-19 09:21:03

6 réponses

le constructeur de copie généré par le compilateur effectue la 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 par membre de stored membre ne dupliquera pas le buffer (seul le pointeur sera copié), donc le premier à être détruit copy sharing le buffer appellera delete[] avec succès et le second fonctionnera dans un comportement non défini. Vous avez besoin de la copie profonde constructeur de copie (et la cession de l'opérateur).

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;
}
64
répondu sharptooth 2010-07-19 08:49:14

je suis un peu peiné que la règle du 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 L'un de Destructor, copy Constructor, Copy Assignment Operator, Move Constructor ou Move Assignment Operator, vous devez probablement écrire les quatre autres.

mais il y a une directive plus générale que vous devez suivre, qui découle de la nécessité d'écrire exception-code de sécurité:

Chaque ressource doit être gérée par un objet

Ici @sharptooth le code est encore (pour la plupart) bien, mais s'il fallait ajouter un deuxième attribut de sa classe, il ne serait pas. 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 new Bar lance ? Comment faire vous supprimez l'objet pointé par mFoo ? Il y a des solutions (niveau de fonction try/catch ...), ils ne sont pas à l'échelle.

la bonne façon de faire face à la situation est d'utiliser des classes appropriées au lieu de pointeurs bruts.

class Righteous
{
public:
private:
  std::unique_ptr<Foo> mFoo;
  std::unique_ptr<Bar> mBar;
};

avec la même implémentation de constructeur (ou en fait, en utilisant make_unique ), j'ai maintenant une sécurité d'exception gratuite!!! N'est-ce pas excitant ? Et le meilleur de tout, je n'ai plus besoin de me soucier d'un destructeur approprié! J'ai besoin d'écrire mes propres Copy Constructor et Assignment Operator cependant, parce que unique_ptr ne définit pas ces opérations... mais il n'est pas question ici ;)

Et donc, sharptooth 's classe a revisité:

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

je ne sais pas pour vous, mais je trouve la mienne plus facile ;)

39
répondu Matthieu M. 2017-05-21 12:50:55

je peux me rappeler de ma pratique et penser aux cas suivants quand on doit traiter explicitement déclarer/définir le constructeur de copie. J'ai regroupé les cas en deux catégories

  • rectitude/sémantique - si vous ne fournissez pas un copy-constructor défini par l'utilisateur, les programmes utilisant ce type peuvent ne pas être compilés, ou peuvent fonctionner incorrectement.
  • optimisation - fourniture une bonne alternative au constructeur de copie généré par le compilateur permet de rendre le programme plus rapide.


Rectitude / Sémantique

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

après avoir lu cette section, vous découvrirez plusieurs pièges de permettre au compilateur de générer seul le constructeur de copies. Par conséquent, comme seand noté dans son réponse , il est toujours sûr de désactiver la possibilité de copie pour une nouvelle classe et délibérément l'activer plus tard quand vraiment nécessaire.

comment rendre une classe non-copiable en C++03

déclarent un copy-constructeur privé et ne fournissent pas une implémentation pour elle (de sorte que le build échoue au stade de lien même si les objets de ce type sont copiés dans le propre scope de la classe ou par ses amis).

comment rendre une classe non-copiable en C++11 ou plus récente

déclare le copy-constructor avec =delete à la fin.


peu profonde vs Copie en Profondeur

C'est le cas le mieux compris et en fait le seul mentionné dans l'autre réponse. shaprtooth a couvert assez bien. Je veux seulement ajouter que copier profondément des ressources qui devraient être exclusivement la propriété de l'objet peut s'appliquer à n'importe quel type de ressources, dont la mémoire dynamiquement allouée n'est qu'un type. Si nécessaire, copier profondément un objet peut également exiger

  • copier des fichiers temporaires sur le disque
  • ouverture d'une connexion réseau séparée
  • création d'un fil ouvrier séparé
  • attributing a separate OpenGL framebuffer
  • etc

objets auto-enregistrables

considérez une classe où tous les objets - peu importe comment ils ont été construits - doivent être enregistrés d'une manière ou d'une autre. Quelques exemples:

  • le exemple le plus simple: maintenir le nombre total d'objets existants. L'enregistrement des objets ne fait qu'incrémenter le compteur statique.

  • un exemple plus complexe est celui d'un registre unique, où sont stockées des références à tous les objets existants de ce type (de sorte que les notifications puissent être livrées à tous).

  • référence comptée smart-les pointeurs peuvent être considérés comme juste un spécial cas dans cette catégorie: le nouveau pointeur "registres" à la ressource partagée plutôt que dans un registre mondial.

une telle opération d'auto-enregistrement doit être effectuée par tout constructeur du type et le constructeur de la copie ne fait pas exception.


objets avec références internes

Certains objets peuvent avoir des structure avec des références croisées directes entre leurs différents sous-objets (en fait, une seule de ces références croisées internes suffit pour déclencher ce cas). Le constructeur de copie fourni par le compilateur cassera les associations internes intra-object , en les convertissant en inter-object associations.

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 objets répondant à certains critères sont autorisés à être copiés

il peut y avoir des classes où les objets peuvent être copiés en toute sécurité dans un certain état (par exemple, default-constructed-state) et pas à copier autrement. Si nous voulons autoriser la copie d'objets sûrs à copier, alors-si la programmation défensivement-nous avons besoin d'une vérification de l'exécution dans le constructeur de copie défini par l'utilisateur.


sous-objets non reproductibles

parfois, une classe qui devrait être des agrégats copiables Non-copiables sous-objets. Habituellement, cela se produit pour les objets 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-objets Quasi-copiables

d'Une classe, qui devrait être copiable, peut agréger un sous-objet d'un quasi-copiable type. Un type quasi copiable ne fournit pas de constructeur de copie au sens strict, mais a un autre constructeur qui permet de créer une copie conceptuelle de l'objet. La raison pour faire un type quasi copiable 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 de l'auto-enregistrement d'objet, nous pouvons faire valoir que il peut y avoir des situations où un objet doit être enregistré auprès du objet responsable uniquement si c'est un objet autonome. Si c'est un sous-objet d'un autre objet, alors la responsabilité de la gestion, c'est avec son objet contenant.

ou, la copie superficielle et la copie profonde doivent être supportées (aucune d'elles n'étant la valeur par défaut).

alors la décision finale est laissée aux utilisateurs de ce type - quand ils copient des objets, ils doivent préciser explicitement (par des arguments supplémentaires) la méthode prévue de copie.

dans le cas d'une approche non défensive de la programmation, il est également possible qu'un copy-constructor régulier et un quasi-copy-constructor soient présents. Cela peut se justifier lorsque, dans la grande majorité des cas, une seule méthode de copie doit être appliquée, alors que, dans des situations rares mais bien comprises, d'autres méthodes de copie doivent être utilisées. Alors le compilateur ne se plaindra pas qu'il est incapable de définir implicitement le constructeur de copie; il sera le seul utilisateur responsabilité de se souvenir et de vérifier si un sous-objet de ce type doit être copié via un quasi-copy-constructor.


Ne copiez pas l'état qui est fortement associée à l'identité de l'objet

dans de rares cas, un sous-ensemble de l'état observable de l'objet peut constituer (ou être considéré comme) une partie inséparable de l'identité de l'objet et ne devrait pas être transférable à d'autres objets (bien que cela puisse être quelque peu controversée).

exemples:

  • L'UID de l'objet (mais celui-ci appartient également au cas de "l'auto-enregistrement" ci-dessus, puisque l'identification doit être obtenue dans un acte d'auto-enregistrement).

  • Histoire de l'objet (par exemple, la fonction Annuler/Rétablir pile) dans le cas où le nouvel objet ne doit pas hériter de l'histoire de l'objet source, mais au lieu de cela, commencez par un élément d'histoire unique " copié à ".

dans ces cas, le constructeur de copie doit sauter la copie des sous-objets correspondants.


application de la signature correcte du constructeur de copie

la signature du constructeur de copie fourni par le compilateur dépend de quelle copie les constructeurs sont disponibles pour les sous-objets. Si au moins un sous-objet n'a pas de constructeur de copie réelle (en prenant l'objet source par référence constante) mais a plutôt un constructeur de copie en mutation (en prenant l'objet source par référence non constante) alors le compilateur n'aura pas d'autre choix que de déclarer implicitement et de définir ensuite un constructeur de copie en mutation.

maintenant, que se passe-t-il si le copy-constructor "mutant" de le type du sous-objet ne mute pas réellement l'objet source (et a simplement été écrit par un programmeur qui ne connaît pas le mot-clé const )? Si nous ne pouvons pas faire corriger ce code en ajoutant le const manquant , alors l'autre option est de déclarer notre propre constructeur de copie défini par l'utilisateur avec une signature correcte et de commettre le péché de tourner vers un const_cast .


Copy-on-write (COW)

un conteneur de vache qui a donné des références directes à ses données internes doit être profondément copié au moment de la construction, sinon il peut se comporter comme une poignée de comptage de référence.

bien que la vache 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.



l'Optimisation

dans les cas suivants, vous pouvez vouloir/avoir besoin de définir votre propre constructeur de copies par souci d'optimisation:


optimisation de la Structure pendant la copie

considérer un conteneur qui supporte les opérations d'enlèvement d'éléments, mais peut le faire en marquant simplement l'élément enlevé comme supprimé, et recycler sa fente plus tard. Lorsqu'une copie d'un tel conteneur est faite, il peut être logique de compacter les données existantes plutôt que de préserver les fentes "supprimées" telles quelles.


éviter la copie non observables de l'état

un objet 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émoizées accumulées au cours de la vie de l'objet afin d'accélérer certaines requête lente les opérations effectuées par l'objet. Il est sûr de ne pas copier ces données car elles seront recalculées quand (et si!) les opérations 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'objet (à partir duquel les données mises en cache sont dérivées) est modifié par des opérations de mutation (et si nous n'allons pas modifier l'objet, pourquoi créer une copie profonde 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ésactivation de la copie implicite

C++ permet de désactiver la copie implicite en déclarant le constructeur de copie explicit . Alors les objets de cette classe ne peuvent pas être passés dans des fonctions et/ou retournés de fonctions par valeur. Cette astuce peut être utilisée pour un type qui semble être léger mais est en effet très coûteux pour copier (cependant, le rendre quasi-copiable pourrait être un meilleur choix).

en C++03 déclarer un constructeur de copie requis le définir aussi (bien sûr, si vous avez l'intention de l'utiliser). Par conséquent, aller pour un tel constructeur de copie simplement de la préoccupation étant discuté signifiait que vous avez dû écrire le même code que le compilateur générerait automatiquement pour vous.

C++11 et les normes plus récentes permettent de déclarer un membre spécial fonctions (la par défaut et les constructeurs de copie, l'opérateur copy-assignment et le destructor) avec une demande explicite d'utiliser la mise en œuvre par défaut (il suffit de terminer la déclaration par =default ).



1519480920"

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

  • ajouter plus de code d'exemple
  • illustre les" objets avec renvois internes "case
  • ajouter des liens

24
répondu Leon 2017-05-23 12:18:25

si vous avez une classe qui a un contenu alloué dynamiquement. Par exemple, vous stockez le titre d'un livre comme un char * et mettez le titre avec nouveau, copie ne fonctionnera pas.

vous devriez écrire un constructeur de copie qui fait title = new char[length+1] et puis strcpy(title, titleIn) . Le constructeur de copie ferait juste une copie" superficielle".

6
répondu Peter Ajtai 2010-07-19 05:24:53

Copy Constructor est appelé lorsqu'un objet est soit passé par la valeur, retourné par la 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 fait une copie superficielle. Si l'objet n'a pas de pointeurs vers la mémoire allouée dynamiquement puis copie le fera.

2
répondu joshu 2010-07-19 05:29:24

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 prévenir les inefficacités telles que la transmission d'un arg 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.

0
répondu seand 2010-07-19 07:49:32