Question Quelle est la règle de trois?


  • Qu'est-ce que copier un objet signifier?
  • Que sont les constructeur de copie et le opérateur d'affectation de copie?
  • Quand dois-je les déclarer moi-même?
  • Comment puis-je empêcher la copie de mes objets?

1841
2017-11-13 13:27


origine


Réponses:


introduction

C ++ traite les variables de types définis par l'utilisateur avec sémantique des valeurs. Cela signifie que les objets sont copiés implicitement dans différents contextes, et nous devrions comprendre ce que signifie "copier un objet".

Considérons un exemple simple:

class person
{
    std::string name;
    int age;

public:

    person(const std::string& name, int age) : name(name), age(age)
    {
    }
};

int main()
{
    person a("Bjarne Stroustrup", 60);
    person b(a);   // What happens here?
    b = a;         // And here?
}

(Si vous êtes intrigué par le name(name), age(age) partie, c'est ce qu'on appelle un liste des initialiseurs de membres.)

Fonctions spéciales des membres

Qu'est-ce que cela signifie de copier un person objet? le main La fonction montre deux scénarios de copie distincts. L'initialisation person b(a); est effectuée par le constructeur de copie. Son travail consiste à construire un nouvel objet basé sur l'état d'un objet existant. La tâche b = a est effectuée par le opérateur d'affectation de copie. Son travail est généralement un peu plus compliqué, car l'objet cible est déjà dans un état valide qui doit être traité.

Puisque nous n'avons déclaré ni le constructeur de la copie ni l'opérateur d'assignation (ni le destructeur), ceux-ci sont implicitement définis pour nous. Citation de la norme:

Le constructeur [...] de copie et l'opérateur d'affectation de copie, [...] et le destructeur sont des fonctions membres spéciales.   [ Remarque: L'implémentation déclarera implicitement ces fonctions membres   pour certains types de classe lorsque le programme ne les déclare pas explicitement.   L'implémentation les définira implicitement s'ils sont utilisés. [...] note de fin ]   [n3126.pdf article 12 §1]

Par défaut, copier un objet signifie copier ses membres:

Le constructeur de copie implicitement défini pour une classe X non-union effectue une copie de ses sous-objets.   [n3126.pdf article 12.8 §16]

L'opérateur d'affectation de copie implicitement défini pour une classe X non syndiquée exécute une affectation de copie par membre.   de ses sous-objets.   [n3126.pdf article 12.8 §30]

Définitions implicites

Les fonctions membres spéciales implicitement définies pour person ressemble à ca:

// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    name = that.name;
    age = that.age;
    return *this;
}

// 3. destructor
~person()
{
}

Copier Memberwise est exactement ce que nous voulons dans ce cas: name et age sont copiés, donc nous obtenons un autonome, indépendant person objet. Le destructeur implicitement défini est toujours vide. C'est également bien dans ce cas puisque nous n'avons pas acquis de ressources dans le constructeur. Les destructeurs des membres sont appelés implicitement après le person destructeur est terminé:

Après avoir exécuté le corps du destructeur et détruit tous les objets automatiques alloués dans le corps,   un destructeur pour la classe X appelle les destructeurs pour les [...] membres directs de X   [n3126.pdf 12.4 §6]

Gérer les ressources

Alors, quand devrions-nous déclarer explicitement ces fonctions spéciales? Quand notre classe gère une ressource, C'est, quand un objet de la classe est responsable pour cette ressource. Cela signifie généralement que la ressource est acquis dans le constructeur (ou passé dans le constructeur) et libéré dans le destructeur.

Revenons dans le passé au C ++ pré-standard. Il n'y avait pas une telle chose std::string, et les programmeurs étaient amoureux de pointeurs. le person La classe aurait pu ressembler à ceci:

class person
{
    char* name;
    int age;

public:

    // the constructor acquires a resource:
    // in this case, dynamic memory obtained via new[]
    person(const char* the_name, int the_age)
    {
        name = new char[strlen(the_name) + 1];
        strcpy(name, the_name);
        age = the_age;
    }

    // the destructor must release this resource via delete[]
    ~person()
    {
        delete[] name;
    }
};

Même aujourd'hui, les gens continuent d'écrire des cours dans ce style et ont des problèmes: "J'ai poussé une personne dans un vecteur et maintenant je reçois des erreurs de mémoire fous!" Rappelez-vous que par défaut, copier un objet signifie copier ses membres, mais en copiant le name le membre copie simplement un pointeur, ne pas le tableau de caractères vers lequel il pointe! Cela a plusieurs effets désagréables:

  1. Changements via a peut être observé via b.
  2. Une fois que b est détruit, a.name est un pointeur qui pend.
  3. Si a est détruit, en supprimant les rendements du pointeur qui pend comportement indéfini.
  4. Puisque la mission ne tient pas compte de ce que name pointé avant la cession, tôt ou tard, vous aurez des fuites de mémoire partout.

Définitions explicites

Comme la copie de membre n'a pas l'effet désiré, nous devons explicitement définir le constructeur de copie et l'opérateur d'affectation de copie pour faire des copies profondes du tableau de caractères:

// 1. copy constructor
person(const person& that)
{
    name = new char[strlen(that.name) + 1];
    strcpy(name, that.name);
    age = that.age;
}

// 2. copy assignment operator
person& operator=(const person& that)
{
    if (this != &that)
    {
        delete[] name;
        // This is a dangerous point in the flow of execution!
        // We have temporarily invalidated the class invariants,
        // and the next statement might throw an exception,
        // leaving the object in an invalid state :(
        name = new char[strlen(that.name) + 1];
        strcpy(name, that.name);
        age = that.age;
    }
    return *this;
}

Notez la différence entre l'initialisation et l'affectation: nous devons démolir l'ancien état avant d'attribuer à name pour éviter les fuites de mémoire. En outre, nous devons protéger contre l'auto-affectation du formulaire x = x. Sans ce contrôle, delete[] name supprimerait le tableau contenant le la source chaîne, parce que quand tu écris x = x, tous les deux this->name et that.name contient le même pointeur.

Sécurité d'exception

Malheureusement, cette solution échouera si new char[...] émet une exception en raison de l'épuisement de la mémoire. Une solution possible consiste à introduire une variable locale et à réorganiser les instructions:

// 2. copy assignment operator
person& operator=(const person& that)
{
    char* local_name = new char[strlen(that.name) + 1];
    // If the above statement throws,
    // the object is still in the same state as before.
    // None of the following statements will throw an exception :)
    strcpy(local_name, that.name);
    delete[] name;
    name = local_name;
    age = that.age;
    return *this;
}

Cela prend également soin de l'auto-affectation sans vérification explicite. Une solution encore plus robuste à ce problème est la copier-et-swap idiome, mais je ne vais pas entrer dans les détails de la sécurité des exceptions ici. J'ai seulement mentionné des exceptions pour faire le point suivant: Écrire des cours qui gèrent des ressources est difficile.

Ressources non-imprimables

Certaines ressources ne peuvent pas ou ne doivent pas être copiées, telles que les handles de fichiers ou les mutex. Dans ce cas, déclarez simplement le constructeur de copie et l'opérateur d'affectation de copie private sans donner de définition

private:

    person(const person& that);
    person& operator=(const person& that);

Alternativement, vous pouvez hériter de boost::noncopyable ou déclarez-les comme supprimés (C ++ 0x):

person(const person& that) = delete;
person& operator=(const person& that) = delete;

La règle de trois

Parfois, vous devez implémenter une classe qui gère une ressource. (Ne jamais gérer plusieurs ressources dans une seule classe, cela ne mènera qu'à la douleur.) Dans ce cas, rappelez-vous règle de trois:

Si vous devez déclarer explicitement le destructeur,   copier le constructeur ou l'opérateur d'affectation de copie,   vous devez probablement les déclarer explicitement tous les trois.

(Malheureusement, cette "règle" n'est pas imposée par le standard C ++ ou tout compilateur dont je suis conscient.)

Conseil

La plupart du temps, vous n'avez pas besoin de gérer vous-même une ressource, parce qu'une classe existante telle que std::string le fait déjà pour vous. Il suffit de comparer le code simple en utilisant un std::string membre à l'alternative compliquée et sujette à erreur en utilisant un char* et vous devriez être convaincu. Tant que vous restez à l'écart des membres pointeurs bruts, la règle des trois est peu susceptible de concerner votre propre code.


1512
2017-11-13 13:27



le Règle de trois est une règle de base pour C ++, en disant essentiellement

Si votre classe a besoin de

  • une constructeur de copie,
  • un opérateur d'assignation,
  • ou un destructeur,

défini explicitement, alors il est susceptible d'avoir besoin tous les trois.

Les raisons en sont que les trois sont généralement utilisés pour gérer une ressource, et si votre classe gère une ressource, elle doit généralement gérer la copie et la libération.

S'il n'y a pas de bonne sémantique pour copier la ressource que votre classe gère, alors pensez à interdire la copie en déclarant (non définir) le constructeur de la copie et l'opérateur d'affectation private.

(Notez que la prochaine version de la norme C ++ (qui est C ++ 11) ajoute la sémantique de déplacement à C ++, ce qui changera probablement la Règle de Trois. Cependant, j'en sais trop peu pour écrire une section C ++ 11 à propos de la Règle des Trois.)


450
2017-11-13 14:22



La loi des trois grands est comme spécifié ci-dessus.

Un exemple facile, en anglais simple, du genre de problème qu'il résout:

Non destructeur par défaut

Vous avez alloué de la mémoire dans votre constructeur et vous devez donc écrire un destructeur pour le supprimer. Sinon, vous provoquerez une fuite de mémoire.

Vous pourriez penser que c'est un travail fait.

Le problème sera, si une copie est faite de votre objet, alors la copie pointera vers la même mémoire que l'objet original.

Une fois, l'un d'eux supprime la mémoire dans son destructeur, l'autre aura un pointeur vers une mémoire invalide (c'est ce qu'on appelle un pointeur) lorsqu'il essayera de l'utiliser, les choses vont devenir poilues.

Par conséquent, vous écrivez un constructeur de copie afin qu'il alloue de nouveaux objets à détruire.

Opérateur d'affectation et constructeur de copie

Vous avez alloué de la mémoire dans votre constructeur à un pointeur de membre de votre classe. Lorsque vous copiez un objet de cette classe, l'opérateur d'affectation par défaut et le constructeur de copie copient la valeur de ce pointeur de membre sur le nouvel objet.

Cela signifie que le nouvel objet et l'ancien objet pointeront sur le même morceau de mémoire, donc quand vous le changez dans un objet, il sera également changé pour l'autre objet. Si un objet supprime cette mémoire, l'autre continuera à essayer de l'utiliser - eek.

Pour résoudre ce problème, vous devez écrire votre propre version du constructeur de copie et de l'opérateur d'affectation. Vos versions allouent de la mémoire séparée aux nouveaux objets et copient les valeurs que le premier pointeur pointe vers plutôt que son adresse.


134
2018-05-14 14:22



Fondamentalement, si vous avez un destructeur (pas le destructeur par défaut), cela signifie que la classe que vous avez définie a une allocation de mémoire. Supposons que la classe soit utilisée à l'extérieur par un code client ou par vous.

    MyClass x(a, b);
    MyClass y(c, d);
    x = y; // This is a shallow copy if assignment operator is not provided

Si MyClass n'a que quelques membres typés primitifs, un opérateur d'affectation par défaut fonctionnera, mais s'il a des membres pointeurs et des objets qui n'ont pas d'opérateurs d'affectation, le résultat sera imprévisible. Par conséquent, nous pouvons dire que s'il y a quelque chose à supprimer dans destructor d'une classe, nous pourrions avoir besoin d'un opérateur de copie profonde, ce qui signifie que nous devrions fournir un constructeur de copie et un opérateur d'affectation.


37
2017-12-31 19:29



Que signifie copier un objet? Il existe plusieurs façons de copier des objets - parlons des 2 types auxquels vous faites le plus référence - copie profonde et copie superficielle.

Puisque nous sommes dans un langage orienté objet (ou du moins le supposons), disons que vous avez un morceau de mémoire alloué. Comme il s'agit d'un langage OO, nous pouvons facilement nous référer à des blocs de mémoire que nous allouons car ce sont généralement des variables primitives (ints, chars, bytes) ou des classes que nous avons définies avec nos propres types et primitives. Alors disons que nous avons une classe de voiture comme suit:

class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;

public changePaint(String newColor)
{
   this.sPrintColor = newColor;
}

public Car(String model, String make, String color) //Constructor
{
   this.sPrintColor = color;
   this.sModel = model;
   this.sMake = make;
}

public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}

public Car(const Car &other) // Copy Constructor
{
   this.sPrintColor = other.sPrintColor;
   this.sModel = other.sModel;
   this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
   if(this != &other)
   {
      this.sPrintColor = other.sPrintColor;
      this.sModel = other.sModel;
      this.sMake = other.sMake;
   }
   return *this;
}

}

Une copie profonde est si nous déclarons un objet, puis créons une copie complètement séparée de l'objet ... nous nous retrouvons avec 2 objets dans 2 ensembles complètement de la mémoire.

Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.

Maintenant, faisons quelque chose d'étrange. Supposons que car2 soit mal programmé ou intentionnellement destiné à partager la mémoire réelle dont est fait car1. Prétendre que chaque fois que vous posez des questions sur car2, vous résolvez vraiment un pointeur sur l'espace mémoire de car1 ... c'est à peu près ce que c'est une copie superficielle est.

//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.

 Car car1 = new Car("ford", "mustang", "red"); 
 Car car2 = car1; 
 car2.changePaint("green");//car1 is also now green 
 delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve 
 the address of where car2 exists and delete the memory...which is also
 the memory associated with your car.*/
 car1.changePaint("red");/*program will likely crash because this area is
 no longer allocated to the program.*/

Donc, peu importe la langue dans laquelle vous écrivez, faites très attention à ce que vous voulez dire quand il s'agit de copier des objets parce que la plupart du temps vous voulez une copie en profondeur.

Quels sont le constructeur de copie et l'opérateur d'affectation de copie? Je les ai déjà utilisés ci-dessus. Le constructeur de copie est appelé lorsque vous tapez du code tel que Car car2 = car1;  Essentiellement, si vous déclarez une variable et que vous l'affectez sur une ligne, le constructeur de la copie est appelé. L'opérateur d'affectation est ce qui se passe lorsque vous utilisez un signe égal--car2 = car1;. Remarquer car2 n'est pas déclaré dans la même déclaration. Les deux morceaux de code que vous écrivez pour ces opérations sont probablement très similaires. En fait, le modèle de conception typique a une autre fonction que vous appelez pour tout configurer une fois que vous êtes satisfait de la copie / affectation initiale est légitime - si vous regardez le code de main-d'œuvre que j'ai écrit, les fonctions sont presque identiques.

Quand dois-je les déclarer moi-même? Si vous n'écrivez pas de code à partager ou à produire d'une manière ou d'une autre, vous n'avez besoin de les déclarer que lorsque vous en avez besoin. Vous devez être conscient de ce que fait votre langage de programmation si vous choisissez de l'utiliser "par accident" et n'en avez pas fait un - c'est-à-dire vous obtenez le compilateur par défaut. J'utilise rarement des constructeurs de copie par exemple, mais les remplacements d'opérateurs d'affectation sont très courants. Saviez-vous que vous pouvez outrepasser ce que l'addition, la soustraction, etc. signifient aussi?

Comment puis-je empêcher la copie de mes objets? Remplacer toutes les façons dont vous êtes autorisé à allouer de la mémoire pour votre objet avec une fonction privée est un bon début. Si vous ne voulez vraiment pas que les gens les copient, vous pouvez le rendre public et alerter le programmeur en lançant une exception et en ne copiant pas l'objet.


27
2017-10-17 16:37



Quand dois-je les déclarer moi-même?

La règle de trois stipule que si vous déclarez un

  1. constructeur de copie
  2. opérateur d'affectation de copie
  3. destructeur

alors vous devriez déclarer tous les trois. Il est né de l'observation que la nécessité de prendre le sens d'une opération de copie découlait presque toujours du fait que la classe effectuait une sorte de gestion des ressources, ce qui impliquait presque toujours que

  • quelle que soit la gestion des ressources en cours d'une opération de copie devait probablement être fait dans l'autre opération de copie et

  • le destructeur de classe participerait également à la gestion de la ressource (généralement en la libérant). La ressource classique à gérer était la mémoire, et c'est pourquoi toutes les classes de la bibliothèque standard gérer la mémoire (par exemple, les conteneurs STL qui exécutent la gestion dynamique de la mémoire) déclarent tous "les trois grands": à la fois les opérations de copie et un destructeur.

Une conséquence de la Règle de Trois est que la présence d'un destructeur déclaré par l'utilisateur indique qu'une copie sage de membre simple est peu susceptible d'être appropriée pour les opérations de copie dans la classe. Cela, à son tour, suggère que si une classe déclare un destructeur, les opérations de copie ne devraient probablement pas être générées automatiquement, car elles ne feraient pas la bonne chose. Au moment de l'adoption de C ++ 98, la signification de cette ligne de raisonnement n'était pas pleinement appréciée, donc en C ++ 98, l'existence d'un destructeur déclaré par l'utilisateur n'avait aucun impact sur la volonté des compilateurs de générer des opérations de copie. Cela continue à être le cas dans C ++ 11, mais seulement parce que restreindre les conditions dans lesquelles les opérations de copie sont générées casserait trop de code hérité.

Comment puis-je empêcher la copie de mes objets?

Déclarez le constructeur de copie et l'opérateur d'affectation de copie en tant que spécificateur d'accès privé.

class MemoryBlock
{
public:

//code here

private:
MemoryBlock(const MemoryBlock& other)
{
   cout<<"copy constructor"<<endl;
}

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
 return *this;
}
};

int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

A partir de C ++ 11, vous pouvez également déclarer le constructeur de copie et l'opérateur d'assignation supprimés

class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};


int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

19
2018-01-12 09:54



La plupart des réponses existantes touchent déjà le constructeur de copie, l'opérateur d'affectation et le destructeur. Cependant, dans l'après C ++ 11, l'introduction de la sémantique de mouvement peut l'étendre au-delà de 3.

Récemment, Michael Claisse a donné une conférence sur ce sujet: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class


9
2018-01-07 05:38



Règle de trois en C ++ est un principe fondamental de la conception et le développement de trois exigences que s'il y a une définition claire dans l'une des fonctions membres suivantes, alors le programmeur doit définir les deux autres fonctions membres ensemble. À savoir, les trois fonctions membres suivantes sont indispensables: destructeur, constructeur de copie, opérateur d'affectation de copie.

Le constructeur de copie en C ++ est un constructeur spécial. Il est utilisé pour construire un nouvel objet, qui est le nouvel objet équivalent à une copie d'un objet existant.

L'opérateur d'affectation de copie est un opérateur d'affectation spécial généralement utilisé pour spécifier un objet existant à d'autres personnes du même type d'objet.

Il y a des exemples rapides:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;

5
2017-08-12 04:27