web-dev-qa-db-fra.com

Quand devons-nous 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?

78
penguru

Le constructeur de copie généré par le compilateur effectue la 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 par membre du membre stored ne dupliquera pas le tampon (seul le pointeur sera copié), donc la première copie à détruire le partage du tampon appellera delete[] avec succès et le second aura un comportement indéfini. Vous avez besoin d'un constructeur de copie de copie approfondie (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;
}
68
sharptooth

Je suis un peu irrité 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 des éléments Destructor, Copy Constructor, Copy Assignment Operator, Move Constructor ou Move Assignment Operator, vous devrez probablement écrire les quatre autres.

Mais il existe une directive plus générale que vous devez 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é

Ici, le code de @sharptooth Est toujours (généralement) correct, mais s'il ajoutait un deuxième attribut à sa classe, ce ne serait pas le cas. 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 Bar Lance? Comment supprimez-vous l'objet pointé par mFoo? Il existe des solutions (niveau fonction try/catch ...), elles ne sont tout simplement pas évolutives.

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 une sécurité d'exception gratuite !!! N'est-ce pas excitant? Et mieux encore, je n'ai plus à me soucier d'un destructeur approprié! J'ai cependant besoin d'écrire mes propres Copy Constructor Et 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 a revisité:

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

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

44
Matthieu M.

Je peux me souvenir de ma pratique et penser aux cas suivants où l'on doit traiter de déclarer/définir explicitement 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 à 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ù déclarer/définir le 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 pièges qui empêchent le compilateur de générer lui-même le constructeur de copie. Par conséquent, comme seand noté dans son answer , il est toujours sûr de désactiver la possibilité de copie pour une nouvelle classe et délibérément de l'activer plus tard lorsque vraiment besoin.

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.


Shallow vs Deep Copy

C'est le cas le mieux compris et en fait le seul mentionné dans les autres réponses. shaprtooth a couvert assez bien. Je veux seulement ajouter que la copie en profondeur des ressources qui devraient appartenir exclusivement à l'objet peut 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 distincte
  • création d'un thread de travail distinct
  • allouer un framebuffer OpenGL séparé
  • etc

Objets auto-enregistrés

Considérez une classe où tous les objets - peu importe comment ils ont été construits - DOIVENT être en quelque sorte enregistrés. Quelques exemples:

  • L'exemple le plus simple: maintenir le nombre total d'objets existants. L'enregistrement d'objet consiste à 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 remises à 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" lui-même auprès de 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 rompra les associations internes intra-objet , les convertissant en inter-objet 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 peuvent être copiés

Il peut y avoir des classes où les objets peuvent être copiés en toute sécurité dans certains états (par exemple, l'état construit par défaut) et pas sûrs pour copier autrement. Si nous voulons autoriser la copie d'objets à copier en toute sécurité, 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 agrège 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 un constructeur de copie au sens strict, mais a un autre constructeur qui permet de créer une copie conceptuelle de l'objet. La raison de rendre un type quasi-copiable est qu'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é auprès du gestionnaire d'objet 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 profonde doit être prise en charge (aucune d'entre elles n'étant 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 être justifié 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 d'être 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 copiez pas 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 doit 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'id doit être obtenu dans un acte d'auto-enregistrement).

  • Historique de l'objet (par exemple la pile Annuler/Rétablir) 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é à <TIME> de <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éelle (en prenant l'objet source par référence constante) mais a plutôt un constructeur de copie mutant (en prenant l'objet source par référence non constante) alors le compilateur n'aura d'autre choix que de déclarer implicitement puis de 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 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 consiste à déclarer notre propre constructeur de copie défini par l'utilisateur avec une signature correcte et à commettre le péché de se tourner vers 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 une poignée 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 pouvez/devez définir votre propre constructeur de copie par souci d'optimisation:


Optimisation de la structure lors de la copie

Envisagez un conteneur qui prend en charge les opérations de suppression d'élément, mais vous pouvez le faire en marquant simplement l'élément supprimé comme supprimé, puis recyclez son emplacement plus tard. Lorsqu'une copie d'un tel conteneur est effectué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 d'ignorer la copie de ces données car elles seront recalculées lorsque (et si!) Les opérations pertinentes seront 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 (dont 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 profond 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 les fonctions et/ou renvoyés des fonctions par valeur. Cette astuce peut être utilisée pour un type qui semble léger mais qui est en effet très cher à 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 ne demande explicite d'utiliser l'implémentation par défaut (il suffit de terminer la déclaration avec =default).



TODO

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

  • Ajoutez plus d'exemple de code
  • Illustrer le cas "Objets avec références croisées internes"
  • Ajoutez des liens
28
Leon

Si vous avez une classe qui a un contenu alloué dynamiquement. Par exemple, vous stockez le titre d'un livre en tant que caractère * et définissez le titre avec new, 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 copie ferait juste une copie "superficielle".

6
Peter Ajtai

Le constructeur de copie 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 crée une copie superficielle. Si l'objet n'a pas de pointeurs vers la mémoire allouée dynamiquement, une copie superficielle fera l'affaire.

2
josh

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 empêcher 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.

0
seand