semantic - rule of zero c++




Quelle est la règle de trois? (6)

Que signifie copier un objet ? Quels sont le constructeur de copie et l' 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?


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 qui gèrent la mémoire (par exemple, les conteneurs STL qui exécutent la gestion dynamique de la mémoire) déclarent «les trois grands»: à la fois des opérations de copie et un destructeur.

Une conséquence de la Règle des Trois est que la présence d'un destructeur déclaré par l'utilisateur indique qu'il est peu probable que la copie sage des membres 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, 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);
}

introduction

C ++ traite les variables de types définis par l'utilisateur avec la sémantique des valeurs . Cela signifie que les objets sont copiés implicitement dans divers contextes, et nous devons comprendre ce que signifie réellement «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 déconcerté par le name(name), age(age) partie d' name(name), age(age) , cela s'appelle une liste d'initialisation de membre .)

Fonctions spéciales des membres

Qu'est-ce que cela signifie de copier un objet 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 copie . Son travail consiste à construire un nouvel objet basé sur l'état d'un objet existant. L'affectation b = a est effectuée par l' 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'affectation (ni le destructeur) nous-mêmes, 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. [ Note : L'implémentation déclarera implicitement ces fonctions membres pour certains types de classes lorsque le programme ne les déclare pas explicitement. L'implémentation les définira implicitement s'ils sont utilisés. [...] note finale ] [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'assignation de copie implicitement défini pour une classe X non syndiquée effectue une attribution de copie par adhérence de ses sous-objets. [n3126.pdf article 12.8 §30]

Définitions implicites

Les fonctions membres spéciales implicitement définies pour la 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()
{
}

Copier Memberwise est exactement ce que nous voulons dans ce cas: le name et l' age sont copiés, ainsi nous obtenons un objet de person indépendant et autonome. 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 la fin du destructeur de person :

Après l'exécution du corps du destructeur et la destruction de 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-à-dire, quand un objet de la classe est responsable de cette ressource. Cela signifie généralement que la ressource est acquise dans le constructeur (ou transmise dans le constructeur) et libérée dans le destructeur.

Revenons dans le passé au C ++ pré-standard. Il n'y avait rien de tel que std::string , et les programmeurs étaient amoureux des pointeurs. La classe de person 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 ennuis: « J'ai poussé une personne dans un vecteur et maintenant je reçois des erreurs de mémoire! » Rappelez-vous que copier un objet signifie par défaut copier ses membres, mais copier le name copie simplement un pointeur, pas le tableau de caractères qu'il pointe! Cela a plusieurs effets désagréables:

  1. Les changements via a peuvent être observés via b .
  2. Une fois que b est détruit, a.name est un pointeur qui pend.
  3. Si a est détruit, la suppression du pointeur dangling génère un comportement indéfini .
  4. Étant donné que l'affectation ne prend pas en compte le name indiqué avant l'affectation, 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 supprimer l'ancien état avant de l'affecter au name pour éviter les fuites de mémoire. En outre, nous devons protéger contre l'auto-affectation de la forme x = x . Sans cette vérification, delete[] name supprimerait le tableau contenant la chaîne source , car lorsque vous écrivez x = x , this->name et that.name contiennent tous deux le même pointeur.

Sécurité d'exception

Malheureusement, cette solution échouera si le new char[...] renvoie 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 l' idiome copy-and-swap , mais je n'entrerai pas dans les détails de la sécurité des exceptions ici. J'ai seulement mentionné les exceptions pour faire le point suivant: écrire des classes 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 comme private sans donner de définition:

private:

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

Vous pouvez également hériter de boost::noncopyable ou les déclarer comme étant 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 la règle de trois :

Si vous devez déclarer explicitement le destructeur, le constructeur de copie 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, car une classe existante telle que std::string déjà pour vous. Comparez simplement le code simple en utilisant un membre std::string à l'alternative alambiquée et sujette aux erreurs 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.


La règle des 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 susceptible d'avoir besoin de 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 (sans defining ) le constructeur de la copie et l'opérateur d'affectation comme 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.)


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 modifié 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.


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 car2 = car1; . Remarque car2 n'est pas déclaré dans la même instruction. 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 programme si vous choisissez de l'utiliser «par accident» et n'en avez pas fait - 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 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 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 l'objet.


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;




rule-of-three