Quelle est la règle des trois?

  • Que signifie copier un object ?
  • Quel est le constructeur de copie et l’ opérateur d’atsortingbution de copie ?
  • Quand dois-je les déclarer moi-même?
  • Comment puis-je empêcher la copie de mes objects?

    introduction

    C ++ traite les variables de types définis par l’utilisateur avec une sémantique de valeur . Cela signifie que les objects sont implicitement copiés dans divers contextes, et que nous devons comprendre ce que signifie “copier un object”.

    Prenons un exemple simple:

     class person { std::ssortingng name; int age; public: person(const std::ssortingng& 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 insortinggué par la partie name(name), age(age) , cela s’appelle une liste d’initialisation des membres .)

    Fonctions membres spéciales

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

    Puisque nous n’avons déclaré ni le constructeur de copie ni l’opérateur d’assignation (ni le destructeur) nous-mêmes, ceux-ci sont implicitement définis pour nous. Citation du standard:

    Le constructeur […] de copie et l’opérateur d’assignation 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 object signifie copier ses membres:

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

    L’opérateur d’affectation de copie implicitement défini pour une classe non-union X effectue une affectation par copie membre de ses sous-objects. [n3126.pdf article 12.8 §30]

    Définitions implicites

    Les fonctions de membre spéciales définies implicitement pour person ressemblent à ceci:

     // 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() { } 

    La copie membre est exactement ce que nous voulons dans ce cas: le name et l’ age sont copiés, nous obtenons donc un object de person indépendant et autonome. Le destructeur défini implicitement est toujours vide. Cela convient également dans ce cas, car nous n’avons acquis aucune ressource dans le constructeur. Les destructeurs des membres sont implicitement appelés après la fin du destructeur de person :

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

    Gestion des ressources

    Alors, quand devrions-nous déclarer ces fonctions spéciales explicitement? Lorsque notre classe gère une ressource , c’est-à-dire lorsqu’un object de la classe est responsable de cette ressource. Cela signifie généralement que la ressource est acquise dans le constructeur (ou transmise au constructeur) et libérée dans le destructeur.

    Retournons dans le temps au C ++ pré-standard. std::ssortingng n’existait pas et les programmeurs étaient amoureux des pointeurs. La classe de person pourrait avoir ressemblé à 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; } }; 

    Encore aujourd’hui, les gens écrivent encore des classes dans ce style et ont des ennuis: ” J’ai poussé une personne dans un vecteur et maintenant j’ai des erreurs de mémoire folles! ” Souvenez-vous que par défaut, copier un object signifie copier ses membres copie simplement un pointeur, pas le tableau de caractères sur lequel il pointe! Cela a plusieurs effets désagréables:

    1. Les modifications via a peuvent être observées via b .
    2. Une fois b détruit, a.name est un pointeur qui se a.name .
    3. Si a est détruit, la suppression du pointeur en suspens génère un comportement indéfini .
    4. Comme l’assignation ne prend pas en compte le name indiqué avant l’affectation, vous obtiendrez tôt ou tard des memory leaks.

    Définitions explicites

    Comme la copie membre n’a pas l’effet souhaité, nous devons définir explicitement le constructeur de copie et l’opérateur d’affectation de copie pour créer des copies complètes 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 supprimer l’ancien état avant d’atsortingbuer un name pour éviter les memory leaks. De plus, nous devons nous protéger contre l’auto-atsortingbution de la forme x = x . Sans cette vérification, delete[] name supprime le tableau contenant la chaîne source , car lorsque vous écrivez x = x , this->name et that.name contiennent le même pointeur.

    Sécurité d’exception

    Malheureusement, cette solution échouera si un new char[...] lance 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 en charge l’auto-affectation sans vérification explicite. Une solution encore plus robuste à ce problème est l’image de copie et d’échange , mais je n’entrerai pas dans les détails de la sécurité des exceptions ici. J’ai seulement mentionné des exceptions pour faire le point suivant: L’écriture de classes qui gèrent des ressources est difficile.

    Ressources non cumulables

    Certaines ressources ne peuvent ou ne doivent pas être copiées, telles que les descripteurs de fichiers ou les mutex. Dans ce cas, déclarez simplement le constructeur de copie et l’opérateur d’assignation de copie comme 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 les déclarer 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 gérez jamais de multiples ressources dans une seule classe, cela ne mènera qu’à la douleur.) Dans ce cas, souvenez-vous de la règle de trois :

    Si vous devez déclarer explicitement le destructeur, le constructeur de la copie ou l’opérateur d’assignation de copie vous-même, vous devrez probablement les déclarer explicitement.

    (Malheureusement, cette “règle” n’est pas appliquée par le standard C ++ ou par un compilateur dont je suis au courant.)

    Conseil

    La plupart du temps, vous n’avez pas besoin de gérer une ressource vous-même, car une classe existante telle que std::ssortingng déjà pour vous. Comparez simplement le code simple en utilisant un membre std::ssortingng à l’alternative compliquée et sujette à erreur en utilisant un caractère char* et vous devriez être convaincu. Tant que vous restz à l’écart des membres de pointeurs bruts, la règle de trois ne concerne probablement pas votre propre code.

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

    Si votre classe a besoin de

    • un constructeur de copie ,
    • un opérateur d’affectation ,
    • ou un destructeur ,

    défini explicitement, alors il est probable qu’ils auront besoin des trois d’entre eux .

    La raison en est que les trois d’entre eux 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 gérée par votre classe, considérez alors qu’il est interdit d’interdire la copie en déclarant (sans définir ) le constructeur de copie et l’opérateur d’assignation comme étant private .

    (Notez que la nouvelle version à venir du standard C ++ (qui est C ++ 11) ajoute la sémantique de déplacement à C ++, ce qui changera probablement la règle de trois. Cependant, je ne sais pas trop comment écrire une section C ++ 11) à propos de la règle des trois.)

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

    Un exemple simple, en anglais, du type de problème qu’il résout:

    Destructeur non 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 allez provoquer une fuite de mémoire.

    Vous pourriez penser que c’est un travail accompli.

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

    Une fois que l’un d’eux supprime la mémoire dans son destructeur, l’autre aura un pointeur sur la mémoire invalide (cela s’appelle un pointeur) quand il essaiera de l’utiliser, les choses vont devenir velues.

    Par conséquent, vous écrivez un constructeur de copie afin qu’il alloue de nouveaux objects à leurs propres morceaux de mémoire à 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 object 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 object.

    Cela signifie que le nouvel object et l’ancien object pointeront sur le même morceau de mémoire. Ainsi, lorsque vous le modifierez dans un object, il sera également modifié pour l’autre object. Si un object supprime cette mémoire, l’autre continuera d’essayer de l’utiliser – eek.

    Pour résoudre ce problème, vous écrivez votre propre version du constructeur de copie et de l’opérateur d’affectation. Vos versions allouent une mémoire distincte aux nouveaux objects et copient les valeurs pointant sur le premier pointeur plutôt que leur adresse.

    Fondamentalement, si vous avez un destructeur (pas le destructeur par défaut), cela signifie que la classe que vous avez définie a une certaine 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 ne comporte que quelques membres typés primitifs, un opérateur d’affectation par défaut fonctionnerait, mais s’il a des membres de pointeur et des objects sans opérateur d’affectation, le résultat serait imprévisible. Par conséquent, nous pouvons dire que s’il y a quelque chose à supprimer dans le destructeur d’une classe, nous pourrions avoir besoin d’un opérateur de copie profonde, ce qui signifie que nous devons fournir un constructeur de copie et un opérateur d’affectation.

    Que signifie copier un object? Il y a plusieurs façons de copier des objects – parlons des 2 types auxquels vous faites très probablement référence – la copie profonde et la copie superficielle.

    Puisque nous sums dans un langage orienté object (ou du moins le supposons), supposons que vous ayez un morceau de mémoire alloué. Comme il s’agit d’un langage OO, nous pouvons facilement nous référer à des morceaux de mémoire que nous allouons car ce sont généralement des variables primitives (ints, chars, octets) ou des classes que nous avons définies et constituées de 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 ssortingngs do not need to be allocated. { private Ssortingng sPrintColor; private Ssortingng sModel; private Ssortingng sMake; public changePaint(Ssortingng newColor) { this.sPrintColor = newColor; } public Car(Ssortingng model, Ssortingng make, Ssortingng 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 ssortingngs, we have nothing additional to handle. //The assumption is being made that the 3 ssortingngs will be handled by ssortingng'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 object puis créons une copie complètement séparée de l’object … nous nous retrouvons avec 2 objects dans 2 ensembles de mémoire complètement.

     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. Disons que car2 est soit mal programmé, soit intentionnellement conçu pour partager la mémoire réelle de car1. (C’est généralement une erreur de faire ceci et dans les classes est généralement la couverture dont il est question ci-dessous.) Imaginez que chaque fois que vous posez des questions sur car2, vous résolvez réellement un pointeur vers la mémoire de car1. 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 objects, car la plupart du temps, vous voulez une copie complète.

    Quel est le constructeur de copie et l’opérateur d’atsortingbution 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 l’assignez dans une ligne, c’est à ce moment que 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; . Remarque 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 définir une fois que vous êtes satisfait de la copie / affectation initiale est légitime – si vous regardez le code long 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 de production d’une manière ou d’une autre, il vous suffit de les déclarer lorsque vous en avez besoin. Vous devez être conscient de ce que fait le langage de votre programme si vous choisissez de l’utiliser “par accident” et que vous n’en avez pas créé un, c’est-à-dire que 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 signifie l’addition, la soustraction, etc.?

    Comment puis-je empêcher la copie de mes objects? Remplacer toutes les façons dont vous êtes autorisé à allouer de la mémoire pour votre object avec une fonction privée est un début raisonnable. 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 non plus l’object.

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

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

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

    alors vous devriez déclarer tous les trois. Il est apparu que le besoin de reprendre le sens d’une opération de copie provenait presque toujours de la classe effectuant une sorte de gestion des ressources, ce qui impliquait presque toujours que

    • quelle que soit la gestion des ressources effectuée en une seule opération de copie devait probablement être effectuée dans l’autre opération de copie et

    • le destructeur de classe participerait également à la gestion de la ressource (en le relâchant généralement). La ressource classique à gérer était la mémoire, et c’est pourquoi toutes les classes de la bibliothèque standard qui gèrent la mémoire (par exemple, les conteneurs STL qui gèrent la mémoire dynamic) déclarent toutes les trois: 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’il est peu probable qu’une copie simple de membre soit 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. En C ++ 98, l’existence d’un destructeur déclaré par l’utilisateur n’avait aucune incidence sur la volonté des compilateurs de générer des opérations de copie. Cela continue à être le cas en C ++ 11, mais uniquement parce que limiter les conditions dans lesquelles les opérations de copie sont générées briserait trop de code hérité.

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

    Déclarez le constructeur de copie et l’opérateur d’assignation de copie comme spécificateur d’access privé.

     class MemoryBlock { public: //code here private: MemoryBlock(const MemoryBlock& other) { cout< <"copy constructor"< 

    En C ++ 11 et suivantes, vous pouvez également déclarer que l'opérateur de copie et l'opérateur d'affectation ont été 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); } 

    De nombreuses réponses existantes touchent déjà le constructeur de la copie, l’opérateur d’affectation et le destructeur. Cependant, en post C ++ 11, l’introduction de la sémantique de mouvement peut aller 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

    La règle de trois en C ++ est un principe fondamental de la conception et du développement de trois conditions que si la définition de la fonction membre suivante est claire, le programmeur doit définir les deux autres fonctions membres. Les trois fonctions membres suivantes sont indispensables: destructeur, constructeur de copie, opérateur d’atsortingbution de copie.

    Le constructeur de copie en C ++ est un constructeur spécial. Il est utilisé pour créer un nouvel object, qui est le nouvel object équivalent à une copie d’un object existant.

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

    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;