Quand devons-nous utiliser des constructeurs de copie?

87

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?

penguru
la source
1
Un des cas pour écrire votre propre copy-ctor: quand vous devez faire une copie profonde. Notez également que dès que vous créez un ctor, aucun ctor par défaut n'est créé pour vous (sauf si vous utilisez le mot-clé par défaut).
harshvchawla

Réponses:

75

Le constructeur de copie généré par le compilateur effectue une copie par membre. Parfois, cela ne suffit pas. 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 du storedmembre par membre ne dupliquera pas le tampon (seul le pointeur sera copié), donc le premier à être détruit en partageant la copie du tampon appellera delete[]avec succès et le second fonctionnera dans un comportement indéfini. Vous avez besoin d'un constructeur de copie de copie profonde (et d'un opérateur d'affectation également).

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;
}
dents acérées
la source
10
Il n'effectue pas de copie bit par bit, mais membre par membre qui invoque en particulier le copy-ctor pour les membres de type classe.
Georg Fritzsche
7
N'écrivez pas l'opérateur d'assingment comme ça. Ce n'est pas une exception sûre. (si le nouveau lève une exception, l'objet est laissé dans un état indéfini avec le magasin pointant sur une partie désallouée de la mémoire (libérez la mémoire UNIQUEMENT après que toutes les opérations pouvant être lancées se soient terminées avec succès)). Une solution simple consiste à utiliser l'idium d'échange de copie.
Martin York
@sharptooth 3e ligne à partir du bas que vous avez delete stored[];et je pense que cela devrait êtredelete [] stored;
Peter Ajtai
4
Je sais que ce n'est qu'un exemple, mais vous devriez souligner que la meilleure solution est d'utiliser std::string. L'idée générale est que seules les classes utilitaires qui gèrent les ressources doivent surcharger les trois grands, et que toutes les autres classes devraient simplement utiliser ces classes utilitaires, éliminant ainsi le besoin de définir l'un des trois grands.
GManNickG
2
@Martin: Je voulais m'assurer qu'il était gravé dans la pierre. : P
GManNickG
46

Je suis un peu énervé que la règle du Rule of Fivene soit pas citée.

Cette règle est très simple:

La règle de cinq :
chaque fois que vous écrivez l'un des éléments suivants: Destructor, Copy Constructor, Copy Assignment Operator, Move Constructor ou Move Assignment Operator, vous devez probablement écrire les quatre autres.

Mais il existe une directive plus générale que vous devriez suivre, qui découle de la nécessité d'écrire du code sans exception:

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

Voici @sharptoothle code « est encore ( la plupart du temps) bien, s'il était d'ajouter un second attribut à sa classe , il ne serait pas. Considérez 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 Barjette? Comment supprimer l'objet pointé par mFoo? Il existe des solutions (niveau de fonction try / catch ...), elles ne sont tout simplement pas mises à l'échelle.

La bonne façon de gérer 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 la sécurité d'exception gratuitement !!! N'est-ce pas excitant? Et le meilleur de tous, je n'ai plus besoin de me soucier d'un destructeur approprié! J'ai besoin d'écrire le mien Copy Constructoret Assignment Operatorcependant, car unique_ptrne définit pas ces opérations ... mais cela n'a pas d'importance ici;)

Et donc, sharptoothla classe de s revisitée:

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

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

Matthieu M.
la source
Pour C ++ 11 - la règle de cinq qui ajoute à la règle de trois le constructeur de déplacement et l'opérateur d'assignation de déplacement.
Robert Andrzejuk
1
@Robb: Notez qu'en fait, comme démontré dans le dernier exemple, vous devez généralement viser la règle de zéro . Seules les classes techniques spécialisées (génériques) devraient se soucier de la gestion d' une ressource, toutes les autres classes devraient utiliser ces pointeurs / conteneurs intelligents et ne pas s'en soucier.
Matthieu M.
@MatthieuM. D'accord :-) J'ai mentionné la règle des cinq, puisque cette réponse est antérieure au C ++ 11 et commence par "Big Three", mais il faut mentionner que maintenant les "Big Five" sont pertinents. Je ne veux pas voter contre cette réponse car elle est correcte dans le contexte demandé.
Robert Andrzejuk
@Robb: Bon point, j'ai mis à jour la réponse pour mentionner la règle des cinq au lieu des trois grands. Espérons que la plupart des gens sont maintenant passés à des compilateurs capables de C ++ 11 (et je plains ceux qui ne l'ont pas encore fait).
Matthieu M.
32

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

  • Exactitude / Sémantique - si vous ne fournissez pas de constructeur de copie défini par l'utilisateur, les programmes utilisant ce type peuvent échouer à se compiler ou fonctionner de manière incorrecte.
  • Optimisation - fournir une bonne alternative au constructeur de copie généré par le compilateur permet de rendre le programme plus rapide.


Exactitude / Sémantique

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

Après avoir lu cette section, vous découvrirez plusieurs écueils permettant au compilateur de générer lui-même le constructeur de copie. Par conséquent, comme seand l'a noté dans sa réponse , il est toujours prudent de désactiver la possibilité de 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 fournissez pas d'implémentation pour celui-ci (de sorte que la construction échoue à l'étape de liaison même si les objets de ce type sont copiés dans la propre portée 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 ou profonde

C'est le cas le mieux compris et en fait le seul mentionné dans les autres réponses. shaprtooth l' a assez bien couvert . Je veux seulement ajouter que les ressources profondément copiées qui devraient être la propriété exclusive de l'objet peuvent s'appliquer à tout type de ressources, dont la mémoire allouée dynamiquement n'est qu'un type. Si nécessaire, la copie en profondeur d'un objet peut également nécessiter

  • copie de fichiers temporaires sur le disque
  • ouverture d'une connexion réseau séparée
  • création d'un thread de travail distinct
  • allocation d'un framebuffer OpenGL séparé
  • etc

Objets auto-enregistrés

Considérons 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:

  • L'exemple le plus simple: maintenir le nombre total d'objets actuellement existants. L'enregistrement d'objets consiste simplement à incrémenter le compteur statique.

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

  • Les pointeurs intelligents comptés par référence peuvent être considérés comme un cas particulier dans cette catégorie: le nouveau pointeur "s'enregistre" 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 objets peuvent avoir une structure interne non triviale avec des références croisées directes entre leurs différents sous-objets (en fait, une seule référence croisée interne suffit pour déclencher ce cas). Le constructeur de copie fourni par le compilateur cassera les associations intra-objets internes , les convertissant en associations inter-objets .

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 peuvent être copiés

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


Sous-objets non copiables

Parfois, une classe qui devrait être copiable regroupe des sous-objets non copiables. Habituellement, cela se produit pour les objets avec un état non observable (ce cas est décrit plus en détail dans la section "Optimisation" ci-dessous). Le compilateur aide simplement à reconnaître ce cas.


Sous-objets quasi-copiables

Une classe, qui devrait être copiable, peut agréger un sous-objet d'un type quasi-copiable. Un type quasi-copiable ne fournit pas de constructeur de copie au sens strict, mais possède un autre constructeur qui permet de créer une copie conceptuelle de l'objet. La raison pour laquelle un type est quasi-copiable est lorsqu'il n'y a pas d'accord complet sur la sémantique de copie du type.

Par exemple, en revisitant le cas d'auto-enregistrement d'objet, nous pouvons affirmer qu'il peut y avoir des situations où un objet doit être enregistré avec le gestionnaire d'objets global uniquement s'il s'agit d'un objet autonome complet. S'il s'agit d'un sous-objet d'un autre objet, la responsabilité de sa gestion incombe à son objet conteneur.

Ou bien, la copie superficielle et la copie profonde doivent être prises en charge (aucune d'entre elles n'est la valeur par défaut).

Ensuite, la décision finale est laissée aux utilisateurs de ce type - lors de la copie d'objets, ils doivent spécifier explicitement (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 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. Ensuite, le compilateur ne se plaindra pas qu'il est incapable de définir implicitement le constructeur de copie; il sera de la seule responsabilité des utilisateurs de se souvenir et de vérifier si un sous-objet de ce type doit être copié via un constructeur de quasi-copie.


Ne pas copier l'état qui est fortement associé à 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é) 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é).

Exemples:

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

  • Historique de l'objet (par exemple la pile Undo / Redo) dans le cas où le nouvel objet ne doit pas hériter de l'historique de l'objet source, mais plutôt commencer par un seul élément d'historique " Copié à <HEURE> depuis <OTHER_OBJECT_ID> ".

Dans de tels cas, le constructeur de copie doit ignorer 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 des constructeurs de copie disponibles pour les sous-objets. Si au moins un sous-objet n'a pas de constructeur de copie réel (prenant l'objet source par référence constante) mais a à la place un constructeur de copie mutant (prenant l'objet source par référence non constante), le compilateur n'aura pas le choix mais pour déclarer implicitement puis définir un constructeur de copie mutant.

Maintenant, que se passe-t-il si le constructeur de copie "mutant" du type du sous-objet ne mute pas réellement l'objet source (et a été simplement écrit par un programmeur qui ne connaît pas le constmot clé)? Si nous ne pouvons pas réparer ce code en ajoutant le code manquant const, 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 passer à un 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 un descripteur de comptage de références.

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 voudrez peut-être / devrez définir votre propre constructeur de copie par souci d'optimisation:


Optimisation de la structure lors de la copie

Considérez un conteneur qui prend en charge les opérations de suppression d'élément, mais qui peut le faire en marquant simplement l'élément supprimé comme supprimé, et recycler son emplacement ultérieurement. Lorsqu'une copie d'un tel conteneur est réalisée, il peut être judicieux de compacter les données survivantes plutôt que de conserver les emplacements "supprimés" tels quels.


Ignorer la copie de l'état non observable

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émorisées accumulées pendant la durée de vie de l'objet afin d'accélérer certaines opérations de requête lentes effectuées par l'objet. Il est prudent de ne pas copier ces données car elles seront recalculées lorsque (et si!) Les opérations pertinentes 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éons-nous un copier 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 copie explicit. Ensuite, les objets de cette classe ne peuvent pas être passés dans des fonctions et / ou renvoyés par des 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 (cependant, le rendre quasi-copiable pourrait être un meilleur choix).

En C ++ 03, déclarer un constructeur de copie nécessitait de le définir également (bien sûr, si vous aviez l'intention de l'utiliser). Par conséquent, opter pour un tel constructeur de copie simplement par souci de discussion signifiait que vous deviez é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 des fonctions membres spéciales (les constructeurs par défaut et de copie, l'opérateur d'affectation de copie et le destructeur) avec une demande explicite d'utiliser 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 quelques liens
Léon
la source
6

Si vous avez une classe qui a un contenu alloué dynamiquement. Par exemple, vous stockez le titre d'un livre sous forme de caractère * et définissez le titre avec new, la copie ne fonctionnera pas.

Vous auriez à écrire un constructeur de copie qui fait title = new char[length+1]et ensuite strcpy(title, titleIn). Le constructeur de copie ferait simplement une copie "superficielle".

Peter Ajtai
la source
2

Copy Constructor est appelé lorsqu'un objet est passé par valeur, renvoyé par valeur ou copié explicitement. 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, une copie superficielle fera l'affaire.

Josh
la source
0

C'est souvent une bonne idée de désactiver copy ctor et operator = sauf si la classe en a spécifiquement besoin. Cela peut éviter des inefficacités telles que le passage d'un argument par valeur lorsque la référence est prévue. Les méthodes générées par le compilateur peuvent également être invalides.

seand
la source
-1

Considérons ci-dessous l'extrait de code:

class base{
    int a, *p;
public:
    base(){
        p = new int;
    }
    void SetData(int, int);
    void ShowData();
    base(const base& old_ref){
        //No coding present.
    }
};
void base :: ShowData(){
    cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
    this->a = a;
    *(this->p) = b;
}
int main(void)
{
    base b1;
    b1.SetData(2, 3);
    b1.ShowData();
    base b2 = b1; //!! Copy constructor called.
    b2.ShowData();
    return 0;
}

Output: 
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();

b2.ShowData();donne une sortie indésirable car il existe un constructeur de copie défini par l'utilisateur créé sans code écrit pour copier les données explicitement. Donc, le compilateur ne crée pas la même chose.

Je pensais juste à partager ces connaissances avec vous tous, même si la plupart d'entre vous le savent déjà.

Bravo ... Bon codage !!!

Pankaj Kumar Thapa
la source