web-dev-qa-db-fra.com

Quelle est la règle de trois?

  • Que signifie copier un objet?
  • Que sont le constructeur de copie et le opérateur d'attribution de copie?
  • Quand dois-je les déclarer moi-même?
  • Comment puis-je empêcher la copie de mes objets?
2036
fredoverflow

Introduction

C++ traite les variables de types définis par l'utilisateur avec sémantique de valeur. Cela signifie que les objets sont copiés implicitement dans différents contextes, et nous devons 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 la partie name(name), age(age), on l'appelle liste d'initialisation des membres .)

Fonctions spéciales des membres

Que signifie copier un objet person? La fonction main affiche deux scénarios de copie distincts. L'initialisation person b(a); est effectuée par le constructeur de la copie. Son travail consiste à construire un nouvel objet en fonction de l'état d'un objet existant. L'affectation b = a est effectuée par opérateur d'attribution de copie. Son travail est généralement un peu plus compliqué, car l’objet cible se trouve déjà dans un état valide à traiter.

Comme nous n'avons déclaré ni le constructeur de copie ni l'opérateur d'affectation (ni le destructeur), ceux-ci sont définis implicitement 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 classe lorsque le programme ne les déclare pas explicitement. l'implémentation les définira implicitement si elles sont utilisées. [...] note de fin] [n3126.pdf section 12 §1]

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

Le constructeur de copie défini implicitement pour une classe X non syndiquée effectue une copie membre par membre de ses sous-objets. [n3126.pdf section 12.8 §16]

L'opérateur d'affectation de copie défini implicitement pour une classe X non syndiquée effectue une affectation de copie membre par membre de ses sous-objets. [n3126.pdf section 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 par membre correspond exactement à ce que nous voulons dans ce cas: name et age sont copiés, nous obtenons donc un objet autonome person indépendant. 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 appelés implicitement une fois le destructeur person terminé:

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

Gérer les ressources

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

Retournons dans le passé vers le C++ pré-standard. std::string n'existait pas, et les programmeurs étaient amoureux des pointeurs. La classe 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 écrivent encore 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 folles!" Rappelez-vous que par défaut, copier un objet signifie en copier membres, mais copier le membre name ne fait que copier un pointeur, not ​​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 que b est détruit, a.name est un pointeur suspendu.
  3. Si a est détruit, la suppression du pointeur suspendu donne comportement non défini .
  4. Puisque l’affectation ne tient pas compte de ce que name avait indiqué avant l’affectation, vous aurez tôt ou tard des fuites de mémoire.

Définitions explicites

Étant donné que la copie par 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: il faut détruire l’ancien état avant d’affecter à name pour éviter les fuites de mémoire. Nous devons également nous 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 le même aiguille.

Sécurité d'exception

Malheureusement, cette solution échouera si new char[...] lève une exception pour cause d’é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 le idiome copie-échange , mais je n’entrerai pas dans les détails de la sécurité des exceptions ici. J'ai seulement mentionné des exceptions pour souligner le point suivant: Écrire des classes qui gèrent les ressources est difficile.

Ressources non copiables

Certaines ressources ne peuvent ou ne doivent pas être copiées, telles que les descripteurs de fichier ou les mutex. Dans ce cas, déclarez simplement le constructeur de copie et l'opérateur d'affectation de copie en tant que 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 supprimés (en C++ 11 et supérieur):

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 plusieurs ressources dans une même classe, cela ne fera que causer des problèmes.) Dans ce cas, rappelez-vous la règle de trois :

Si vous devez déclarer explicitement vous-même le destructeur, le constructeur de copie ou l'opérateur d'affectation de copie, vous devez probablement déclarer explicitement les trois.

(Malheureusement, cette "règle" n'est pas appliquée par le standard C++ ni par aucun compilateur à ma connaissance.)

La règle de cinq

À partir de C++ 11, un objet a 2 fonctions membres spéciales supplémentaires: le constructeur de déplacement et l'affectation de déplacement. La règle des cinq États pour mettre en œuvre ces fonctions également.

Un exemple avec les signatures:

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

public:
    person(const std::string& name, int age);        // Ctor
    person(const person &) = default;                // Copy Ctor
    person(person &&) noexcept = default;            // Move Ctor
    person& operator=(const person &) = default;     // Copy Assignment
    person& operator=(person &&) noexcept = default; // Move Assignment
    ~person() noexcept = default;                    // Dtor
};

La règle de zéro

La règle de 3/5 est également appelée règle de 0/3/5. La partie zéro de la règle indique que vous n'êtes autorisé à écrire aucune fonction membre spéciale lors de la création de votre classe.

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::string le fait déjà pour vous. Il suffit de comparer le code simple utilisant un membre std::string à l’alternative compliquée et sujette aux erreurs utilisant un char* et vous devriez être convaincu. Tant que vous restez à l'écart des membres de pointeurs bruts, il est peu probable que la règle de trois concerne votre propre code.

1683
fredoverflow

Rule of Three est une règle de base pour C++, qui dit en gros

Si votre classe a besoin de

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

défini de manière explicite, il est alors probable que vous aurez besoin de tous les trois .

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 ainsi que la libération.

S'il n'y a pas de bonne sémantique pour copier la ressource gérée par votre classe, envisagez d'interdire la copie en déclarant (pas définissant) le constructeur de copie et l'opérateur d'affectation comme private.

(Notez que la nouvelle version à venir du standard C++ (C++ 11) ajoute la sémantique de déplacement à C++, ce qui modifiera probablement la règle de trois. Cependant, je connais trop peu cette procédure pour écrire une section C++ 11. à propos de la règle de trois.)

480
sbi

La loi des trois grands est telle que spécifiée ci-dessus.

Voici un exemple simple du genre 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 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 sur 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 sur la mémoire invalide (on l’appelle pointeur suspendu) quand il essaiera de l’utiliser, les choses vont devenir poilues.

Par conséquent, vous écrivez un constructeur de copie pour qu'il attribue aux nouveaux objets leur propre fragment de mémoire à détruire.

opérateur d'assignation et constructeur de copie

Vous avez alloué de la mémoire dans votre constructeur à un pointeur 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 seront dirigés vers le même morceau de mémoire. Ainsi, lorsque vous le modifiez dans un objet, il le sera également pour l'autre objet. Si un objet supprime cette mémoire, l'autre continuera d'essayer de l'utiliser - eek.

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

151
Stefan

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 des membres de type primitif, un opérateur d’affectation par défaut fonctionnerait, mais s’il comportait des membres de pointeur et des objets n’ayant pas d’opérateurs 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 devrions fournir un constructeur de copie et un opérateur d'affectation.

42
fatma.ekici

Que signifie copier un objet? Il existe plusieurs façons de copier des objets - parlons des deux types auxquels vous faites probablement 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 faire référence à des blocs 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 qui sont 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 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 consiste à déclarer un objet, puis à en créer une copie distinctement distincte ... on se retrouve avec 2 objets dans 2 ensembles de mémoire complets.

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.

Faisons maintenant quelque chose d'étrange. Supposons que car2 soit soit mal programmé, soit délibérément destiné à partager la mémoire dont est constituée car1. (C'est généralement une erreur de faire cela et en classe, c'est généralement la couverture qui fait l'objet de la discussion.) Imaginez que chaque fois que vous posez une question à propos de car2, vous résolvez réellement le pointeur sur l'espace mémoire de car1 ... c'est plus ou moins ce qu'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.*/

Ainsi, quelle que soit la langue dans laquelle vous écrivez, faites très attention à ce que vous entendez par copie d'objets, car vous souhaitez généralement 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 l'attribuez sur une ligne, le constructeur de la copie est appelé. L'opérateur d'affectation est ce qui se produit lorsque vous utilisez un signe égal --car2 = car1;. Remarquez que 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 définir une fois que vous êtes convaincu que la copie/affectation initiale est légitime: si vous regardez le code de code que j'ai écrit, les fonctions sont presque identiques.

Quand dois-je les déclarer moi-même? Si vous n'écrivez pas un code qui doit être partagé ou destiné à la production, il vous suffit de le déclarer quand vous en avez besoin. Vous devez être conscient de ce que fait votre langue de programme si vous choisissez de l’utiliser "par accident" sans en créer une - c’est-à-dire. vous obtenez la valeur par défaut du compilateur. J'utilise rarement des constructeurs de copie, par exemple, mais les remplacements d'opérateurs d'attribution sont très courants. Saviez-vous que vous pouvez outrepasser la signification de l'addition, de la soustraction, etc.?

Comment puis-je empêcher la copie de mes objets? Remplacer tous les moyens par lesquels vous êtes autorisé à allouer de la mémoire à 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.

35
user1701047

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

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

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

alors vous devriez déclarer tous les trois. Cela découlait de l'observation selon laquelle la nécessité 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 dans une opération de copie, il est probable que l'opération de copie doit être effectuée dans l'autre opération de copie et

  • le destructeur de classe participerait également à la gestion de la ressource (le libérant généralement). La ressource classique à gérer était la mémoire, raison pour laquelle toutes les classes de la bibliothèque standard gérant la mémoire (par exemple, les conteneurs STL exécutant une gestion dynamique de la mémoire) déclarent toutes "les trois grands": à la fois des opérations de copie et un destructeur.

ne 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 que la copie d'un simple 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, l’importance de cette ligne de raisonnement n’était pas pleinement comprise. L’existence d’un destructeur déclaré par l’utilisateur n’a donc 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 uniquement parce que restreindre les conditions dans lesquelles les opérations de copie sont générées aurait pour effet de détruire 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);
}

À partir de C++ 11, vous pouvez également déclarer le constructeur de copie et l'opérateur d'affectation 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);
}
24
Ajay yadav

La plupart des réponses existantes touchent déjà le constructeur de la copie, l'opérateur d'affectation et le destructeur. Cependant, après C++ 11, l’introduction de la sémantique de déplacement 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

14
wei

La règle de trois en C++ est un principe fondamental de la conception et du développement de trois exigences, à savoir que s'il existe une définition claire dans l'une des fonctions membres suivantes, le programmeur doit définir ensemble les deux autres fonctions membres. A 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 créer 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 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;
9
Marcus Thornton