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?
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?
Réponses:
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 stored
membre 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;
}
delete stored[];
et je pense que cela devrait êtredelete [] stored;
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.
Je suis un peu énervé que la règle du Rule of Five
ne 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 @sharptooth
le 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 Bar
jette? 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 Constructor
et Assignment Operator
cependant, car unique_ptr
ne définit pas ces opérations ... mais cela n'a pas d'importance ici;)
Et donc, sharptooth
la 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;)
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
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.
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).
Déclarez le constructeur de copie avec =delete
à la fin.
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
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.
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?
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.
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.
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.
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.
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 const
mot 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
.
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.
Dans les cas suivants, vous voudrez peut-être / devrez définir votre propre constructeur de copie par souci d'optimisation:
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.
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.
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
).
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
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".
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.
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.
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 !!!