Quelle est La Règle de Trois?

  • que signifie copier un objet ?
  • Quels sont les constructeur de copie et le copier l'opérateur d'affectation ?
  • Quand dois-je déclarer moi-même?
  • Comment puis-je empêcher la copie de mes objets?
1876
demandé sur Rann Lifshitz 2010-11-13 16:27:09

8 réponses

Introduction

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

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 perplexe par la partie name(name), age(age) ), cela s'appelle une liste d'initialiseur de membre .)

fonctions spéciales des membres

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

puisque nous n'avons déclaré ni le constructeur de copies ni l'opérateur de tâche (ni le destructeur) nous-mêmes, ces sont définis implicitement pour nous. Citation de la norme:

The [...] constructeur de copie et l'opérateur d'assignation de copie, [...] et le destructeur sont spéciales des fonctions membres. [ Note : la mise en œuvre déclarera implicitement ces fonctions de membre pour certains types de classe lorsque le programme ne les déclare pas explicitement. La mise en œuvre implicitement définir si elles sont utilisées. [...] note de fin ] [n3126.pdf de l'article 12 §1]

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

le implicitement-défini le constructeur de copie pour une classe de non-union x effectue une copie de membre de ses sous-objets. [n3126.section pdf 12.8 §16]

l'opérateur d'assignation de copie implicitement défini pour une classe Non syndicale x exécute l'assignation de copie par membre de ses sous-objets. [n3126.section pdf 12.8 §30]

définitions implicites

les fonctions spéciales de membre implicitement définies pour person look comme 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 est exactement ce que nous voulons dans ce cas: name et age sont copiés, donc nous obtenons un objet autonome, indépendant person . Le destructeur défini implicitement est toujours vide. C'est également très bien dans ce cas puisque nous n'avons pas acquis de ressources dans le constructeur. Les destructeurs des membres sont implicitement appelés après que le destructeur person est terminé:

après avoir exécuté le corps du destructeur et détruit tous les objets automatiques alloués à l'intérieur du corps, un destructeur pour la classe X appelle les destructeurs pour X direct [...] membre [n3126.pdf 12.4 §6]

gestion des ressources

alors quand devrions-nous déclarer explicitement ces fonctions spéciales de membre? Lorsque notre classe gère une ressource , c'est-à-dire, lorsqu'un objet de la classe est responsable pour cette ressource. Cela signifie habituellement que la ressource est acquise dans le constructeur. (ou passé dans le constructeur) et libéré dans le destructeur.

revenons dans le temps au pré-standard C++. Il n'y avait pas de std::string , 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 problèmes.: " j'ai poussé une personne dans un vecteur et maintenant j'ai des erreurs de mémoire folles! " Rappelez-vous que par défaut, copier un objet signifie copier ses membres., mais copier le membre name ne fait que Copier 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 suspendu.
  3. si a est détruit, la suppression du pointeur pendulaire donne comportement non défini .
  4. étant donné que la cession ne tient pas compte de ce que name indiquait avant la cession, tôt ou tard, tu auras des trous de mémoire partout.

définitions explicites

puisque la copie par membre n'a pas l'effet désiré, nous devons définir explicitement le constructeur de copie et l'opérateur d'assignation 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;
}

noter la différence entre l'initialisation et l'assignation: nous devons démolir l'ancien état avant d'assigner à name pour éviter les fuites de mémoire. Aussi, nous devons nous protéger contre l'auto-attribution de la formulaire x = x . Sans cette vérification, delete[] name supprimerait le tableau contenant la chaîne source , parce que quand vous écrivez x = x , les deux this->name et that.name contiennent le même pointeur.

sécurité D'Exception

malheureusement, cette solution échouera si new char[...] jette une exception due à l'épuisement de la mémoire. Une solution possible est d'introduire une variable locale et de réorganiser les statements:

// 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-assignation sans une vérification explicite. Une solution encore plus robuste à ce problème est le copie-et-échange idiom , mais je n'entrerai pas dans les détails de la sécurité d'exception ici. J'ai seulement mentionné des exceptions pour faire le point suivant: écrire des classes qui gèrent les ressources est difficile.

ressources non disponibles

Certaines ressources ne peuvent pas ou ne doivent pas être copiées, comme les poignées de fichiers ou les Mutex. Dans ce cas, déclarez simplement le constructeur de copies et l'opérateur 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é (C++0x):

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

la règle des trois

parfois, vous avez besoin de mettre en œuvre une classe qui gère un ressources. (Ne jamais gérer plusieurs ressources dans une seule classe, cela ne fera que conduire à la douleur.) Dans ce cas, rappelez-vous la règle des trois :

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

(Malheureusement, cette "règle" n'est pas appliquée par la norme C++ ou tout compilateur, je suis conscient de.)

Conseil

la Plupart du temps, vous n'avez pas besoin de gérer une ressource de vous-même, parce qu'une classe existante comme std::string déjà fait pour vous. Il suffit de comparer 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 de raw pointer, la règle des trois est peu susceptible de concerner votre propre code.

1542
répondu fredoverflow 2017-05-23 10:31:37

la "règle des trois est une règle empirique pour C++, qui dit 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 probablement besoin de tous les trois .

Les raisons pour cela est que tous les trois d'entre eux sont généralement utilisés pour gérer une ressource, et si votre classe gère une ressource, il est généralement nécessaire de gérer la copie libérer.

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

(notez que la nouvelle version à venir de la norme C++ (qui est C++11) ajoute la sémantique de déplacement à C++, ce qui va probablement changer la règle de trois. Cependant, j'en sais trop peu pour écrire une section C++11 sur la règle des trois.)

456
répondu sbi 2017-05-23 12:03:09

la loi des trois grands est comme indiqué 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 causerez une fuite de mémoire.

Vous pourriez penser que c'est du travail fait.

le le problème sera, si une copie de votre objet, puis la copie de point à la même mémoire que l'objet d'origine.

une fois, l'un d'eux supprime la mémoire dans son destructeur, l'autre aura un pointeur vers la mémoire invalide (ceci est appelé un pointeur pendulaire) quand il essaie de l'utiliser les choses vont devenir velues.

donc, vous écrivez un constructeur de copie de sorte qu'il attribue de nouveaux objets de leurs propres pièces de mémoire à détruire.

opérateur d'Affectation et de constructeur de copie

vous avez attribué de la mémoire dans votre constructeur à un pointeur membre de votre classe. Lorsque vous copiez un objet de cette classe, l'opérateur de tâche par défaut et le constructeur de copie copient la valeur de ce pointeur de membre vers le nouvel objet.

cela signifie que le nouvel objet et l'ancien objet seront dirigés vers le même morceau de mémoire donc quand vous le Changez Dans un objet il sera changé pour l'autre objerct aussi. Si un objet efface cette mémoire l'autre continuera à essayer de l'utiliser - eek.

pour résoudre ceci vous écrivez votre propre version du constructeur de copie et de l'opérateur d'affectation. Vos versions attribuent une mémoire séparée aux nouveaux objets et copient à travers les valeurs que le premier pointeur pointe plutôt que son adresse.

137
répondu Stefan 2018-01-09 18:27:49

essentiellement 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 n'a que quelques membres tapés primitifs, un opérateur d'affectation par défaut fonctionnerait, mais s'il a des membres pointeurs et des objets qui n'ont pas d'opérateurs d'affectation, le résultat serait imprévisible. Nous pouvons donc dire que si 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 de tâche.

39
répondu fatma.ekici 2015-09-11 11:39:15

que signifie copier un objet? Il y a quelques façons de copier des objets--parlons des deux types auxquels vous faites très probablement référence--la copie profonde et la copie superficielle.

puisque nous sommes dans un langage orienté objet (ou du moins le présumons), disons que vous avez un morceau de mémoire alloué. Puisqu'il s'agit d'un langage OO, nous pouvons facilement faire référence à des morceaux de mémoire que nous attribuons parce qu'ils sont généralement des variables primitives (ints, chars, octets) ou des classes que nous avons définies. qui sont faits de nos propres types et primitives. Donc, 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 et créons alors une copie complètement séparée de l'objet...nous nous retrouvons avec 2 objets dans 2 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. Disons que les personnages car2 est soit mal programmée ou délibérément faits pour partager la mémoire réelle car1. (Il est généralement une erreur de faire cela et dans les classes est généralement la couverture, il est discuté sous. Prétendez que chaque fois que vous posez des questions sur car2, vous résolvez vraiment un pointeur vers l'espace mémoire de car1...c'est plus ou moins ce qu'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 profonde.

Qu'est-ce que le constructeur de copie et le opérateur d'assignation de copie? J'ai déjà utilisé plus haut. Le constructeur de copie est appelé lorsque vous tapez un code tel que Car car2 = car1; essentiellement si vous déclarez une variable et l'assignez dans une ligne, c'est quand le constructeur de copie est appelé. L'opérateur de tâche est ce qui se passe lorsque vous utilisez un signe égal-- car2 = car1; . L'avis 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 similaire. En fait, le design typique pattern a une autre fonction que vous appelez pour tout régler une fois que vous êtes convaincu que la copie/assignation initiale est légitime--si vous regardez le code à la main que j'ai écrit, les fonctions sont presque identiques.

Quand dois-je déclarer moi-même? Si vous n'écrivez pas le code qui doit être partagé ou pour la production d'une certaine manière, vous n'avez vraiment besoin de les déclarer que lorsque vous en avez besoin. Vous devez être conscient de ce que fait votre langue de programme si vous choisissez de l'utiliser ' par accident et n'a pas un--c'est à dire que vous obtenez le compilateur par défaut. J'utilise rarement copie constructeurs par exemple, mais l'opérateur d'assignation de remplacements sont très fréquents. Saviez-vous que vous pouvez remplacer ce que l'addition, la soustraction, etc. dire ainsi?

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

30
répondu user1701047 2013-12-12 13:09:19

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

La Règle des Trois états que si vous déclarez une

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

alors vous devez déclarer les trois. Elle est née du constat que la nécessité de prendre sur la signification d'une opération de copie presque toujours

  • quelle que soit la gestion des ressources effectuée dans une opération de copie, il fallait probablement le faire dans l'autre opération de copie et

  • le destructeur de classe participera en outre à la gestion de la ressource (généralement de le libérer). La ressource classique à gérer était la mémoire, et ceci est pourquoi toutes les classes de bibliothèque Standard que gérer la mémoire (par exemple, les conteneurs STL qui effectuent la gestion dynamique de la mémoire) tous 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 que la simple copie conforme à l'utilisateur 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, parce qu'elles ne feraient pas la bonne chose. Au moment de l'adoption de C++98, l'importance de ce raisonnement n'était pas pleinement appréciée, de sorte que dans C++98, l'existence d'un destructeur déclaré par l'utilisateur n'a eu aucun impact sur la volonté des compilateurs de générer des opérations de copie. Cela continue d'être le cas en C++11, mais seulement parce que la restriction des conditions dans lesquelles les opérations de copie sont générées ça briserait trop de code d'héritage.

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

Déclarer constructeur de copie de copie et l'opérateur d'assignation comme l'accès privé spécificateur.

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 que le constructeur de copies et l'opérateur de cession 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);
}
21
répondu Ajay yadav 2016-01-12 10:19:01

plusieurs des réponses existantes touchent déjà le constructeur de copies, l'opérateur d'affectation et le destructeur. Toutefois, après C++11, l'introduction de la sémantique move peut étendre cette possibilité au-delà de 3.

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

11
répondu wei 2015-01-07 05:38:51

règle de trois dans 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 de membre suivantes, alors le programmeur devrait définir les deux autres fonctions de membre ensemble. À savoir les trois fonctions de membre sont indispensables: destructeur, constructeur de copie, copie opérateur d'affectation.

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

Copier l'opérateur d'affectation est un opérateur d'affectation spéciale qui est habituellement 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;
7
répondu Marcus Thornton 2016-10-16 04:57:08