Vraag Wat is de regel van drie?


  • Wat doet een object kopiëren gemiddelde?
  • Wat zijn de kopieer constructor en de kopieer opdracht operator?
  • Wanneer moet ik ze zelf aangeven?
  • Hoe kan ik voorkomen dat mijn objecten worden gekopieerd?

1841
2017-11-13 13:27


oorsprong


antwoorden:


Invoering

C ++ behandelt variabelen van door de gebruiker gedefinieerde typen met waarde semantiek. Dit betekent dat objecten impliciet worden gekopieerd in verschillende contexten, en we moeten begrijpen wat 'het kopiëren van een object' eigenlijk betekent.

Laten we een eenvoudig voorbeeld beschouwen:

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?
}

(Als je verbaasd bent over de name(name), age(age) een deel, dit wordt a genoemd led initializer lijst.)

Speciale ledenfuncties

Wat betekent het om een ​​te kopiëren person voorwerp? De main functie toont twee verschillende kopieerscenario's. De initialisatie person b(a); wordt uitgevoerd door de kopieer constructor. Het is zijn taak om een ​​nieuw object te construeren op basis van de staat van een bestaand object. De opdracht b = a wordt uitgevoerd door de kopieer opdracht operator. Zijn taak is over het algemeen een beetje ingewikkelder, omdat het doelobject zich al in een of andere geldige status bevindt die moet worden afgehandeld.

Aangezien we noch de constructeur van de kopie, noch de operator van de opdracht (noch de destructor) zelf hebben verklaard, deze zijn impliciet gedefinieerd voor ons. Citaat uit de standaard:

De [...] kopieconstructor en kopie-toewijzingsoperator, [...] en destructor zijn speciale lidfuncties.   [ Notitie: De implementatie impliceert impliciet deze lidfuncties   voor sommige klassentypes wanneer het programma ze niet expliciet declareert.   De implementatie definieert ze impliciet als ze worden gebruikt. [...] eindnoot ]   [n3126.pdf sectie 12, §1]

Kopiëren van een object betekent standaard het kopiëren van de leden:

De impliciet gedefinieerde copy-constructor voor een niet-unionklasse X voert een lid-kopie van de subobjecten ervan uit.   [n3126.pdf sectie 12.8 §16]

De impliciet gedefinieerde kopie-toewijzingsoperator voor een niet-verbondsklasse X voert de ledengewijze kopieertoewijzing uit   van zijn subobjecten.   [n3126.pdf sectie 12.8 §30]

Impliciete definities

De impliciet gedefinieerde speciale ledenfuncties voor person er uitzien als dit:

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

Het kopiëren in de juiste richting is precies wat we in dit geval willen: name en age worden gekopieerd, dus we krijgen een onafhankelijke, onafhankelijke person voorwerp. De impliciet gedefinieerde destructor is altijd leeg. Dit is ook goed in dit geval omdat we geen bronnen hebben verkregen in de constructor. De destructors van de leden worden impliciet genoemd naar de person destructor is voltooid:

Na het uitvoeren van het lichaam van de destructor en het vernietigen van automatische objecten die in het lichaam zijn toegewezen,   een destructor voor klasse X noemt de destructors voor X's directe [...] leden   [n3126.pdf 12.4 §6]

Middelen beheren

Dus wanneer moeten we die speciale lidfuncties expliciet aangeven? Wanneer onze klas beheert een bron, dat is, wanneer een object van de klas is verantwoordelijk voor die bron. Dat betekent meestal dat de resource dat is verwierf in de constructor (of doorgegeven aan de constructor) en vrijgelaten in de destructor.

Laten we teruggaan in de tijd om C ++ vooraf te standaardiseren. Er was niet zoiets als std::stringen programmeurs waren verliefd op wijzers. De person klas zou er misschien zo hebben uitgezien:

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;
    }
};

Zelfs vandaag schrijven mensen nog steeds lessen in deze stijl en komen ze in de problemen: "Ik duwde een persoon in een vector en nu krijg ik gekke geheugenfouten!" Vergeet niet dat het kopiëren van een object standaard betekent dat de leden moeten worden gekopieerd, maar het kopiëren van de name lid kopieert alleen een aanwijzer, niet de karakterarray waarnaar het verwijst! Dit heeft verschillende onplezierige effecten:

  1. Veranderingen via a kan worden waargenomen via b.
  2. Een keer b is vernietigd, a.name is een hangende aanwijzer.
  3. Als a is vernietigd, het verwijderen van de bungelende wijzer opbrengsten ongedefinieerd gedrag.
  4. Omdat de opdracht geen rekening houdt met wat name gewezen vóór de opdracht, vroeg of laat krijg je overal geheugenlekken.

Expliciete definities

Omdat ledenkopiëren niet het gewenste effect heeft, moeten we de copy constructor en de copy assignment-operator expliciet definiëren om diepe kopieën van de karakterarray te maken:

// 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;
}

Let op het verschil tussen initialisatie en toewijzing: we moeten de oude staat afbreken voordat we hem toewijzen name om geheugenlekken te voorkomen. We moeten ons ook beschermen tegen zelfbestemming van het formulier x = x. Zonder die controle, delete[] name zou de array verwijderen die de bevat bron draad, omdat als je schrijft x = x, beide this->name en that.name bevatten dezelfde aanwijzer.

Uitzonderingsveiligheid

Helaas zal deze oplossing falen als new char[...] werpt een uitzondering vanwege geheugenuitputting. Een mogelijke oplossing is om een ​​lokale variabele in te voeren en de instructies opnieuw in te delen:

// 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;
}

Dit zorgt ook voor zelfopdracht zonder een expliciete controle. Een nog robuustere oplossing voor dit probleem is de idiom kopiëren en uitwisselen, maar ik zal hier niet ingaan op de details van de uitzonderingsveiligheid. Ik noemde alleen uitzonderingen om het volgende punt te maken: Het schrijven van klassen die bronnen beheren, is moeilijk.

Niet-kopieerbare bronnen

Sommige bronnen kunnen of mogen niet worden gekopieerd, zoals bestandshandvatten of mutexen. In dat geval declareert u gewoon de copy constructor en copy assignment-operator als private zonder een definitie te geven:

private:

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

Als alternatief kunt u erven van boost::noncopyable of declareren als verwijderd (C ++ 0x):

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

De regel van drie

Soms moet u een klasse implementeren die een bron beheert. (Nooit meerdere bronnen beheren in een enkele klasse, dit zal alleen maar tot pijn leiden.) Onthoud in dat geval het regel van drie:

Als u de destructor expliciet moet aangeven,   kopieer de constructor of kopieer opdrachtoperator zelf,   je moet ze waarschijnlijk alle drie expliciet declareren.

(Helaas wordt deze "regel" niet afgedwongen door de C ++ -standaard of een andere compiler die ik ken.)

Advies

Meestal hoeft u zelf geen bron te beheren, omdat een bestaande klasse zoals std::string doet het al voor u. Vergelijk gewoon de eenvoudige code met behulp van a std::string lid naar het ingewikkelde en foutgevoelige alternatief met behulp van een char* en je moet overtuigd zijn. Zolang u wegblijft van onbewerkte pointerleden, is het onwaarschijnlijk dat de regel van drie betrekking heeft op uw eigen code.


1512
2017-11-13 13:27



De Regel van Drie is een vuistregel voor C ++, eigenlijk zeggen

Als je klas er een nodig heeft

  • een kopieer constructor,
  • een toewijzingsoperator,
  • of a destructor,

uitvoerbaar gedefinieerd, dan is het waarschijnlijk nodig alle drie.

De redenen hiervoor zijn dat ze alle drie meestal worden gebruikt om een ​​resource te beheren. Als uw klas een resource beheert, moet deze meestal zowel kopiëren als bevrijden beheren.

Als er geen goede semantiek is voor het kopiëren van de bron die uw klas beheert, overweeg dan om te kopiëren te verbieden door te verklaren (niet definiëren) de kopieerconstructor en toewijzingsoperator als private.

(Merk op dat de komende nieuwe versie van de C ++ -standaard (die C ++ 11 is) de verplaatsingssemantiek toevoegt aan C ++, wat de regel van de drie waarschijnlijk zal veranderen. Ik weet echter te weinig hierover om een ​​C ++ 11-sectie te schrijven over de Regel van Drie.)


450
2017-11-13 14:22



De wet van de grote drie is zoals hierboven aangegeven.

Een eenvoudig voorbeeld, in gewoon Engels, van het soort probleem dat het oplost:

Niet standaard destructor

Je hebt geheugen toegewezen aan je constructor en dus moet je een destructor schrijven om het te verwijderen. Anders veroorzaakt u een geheugenlek.

Je zou kunnen denken dat dit een klus is.

Het probleem zal zijn dat als een kopie van uw object is gemaakt, de kopie naar hetzelfde geheugen verwijst als het oorspronkelijke object.

Eens, één van deze verwijdert het geheugen in zijn destructor, de andere zal een wijzer naar een ongeldig geheugen hebben (dit wordt een bungelende wijzer genoemd) wanneer het probeert om het te gebruiken, zullen dingen behaard worden.

Daarom schrijf je een copy-constructor zodat deze nieuwe objecten hun eigen geheugen toewijst om te vernietigen.

Opdrachtoperator en kopieconstructor

Je hebt geheugen in je constructor toegewezen aan een lidwijzer van je klas. Wanneer u een object van deze klasse kopieert, kopieert de standaardtoewijzingsoperator en kopieerconstructor de waarde van deze lidwijzer naar het nieuwe object.

Dit betekent dat het nieuwe object en het oude object naar hetzelfde geheugen zullen wijzen, dus wanneer u het in één object wijzigt, zal het voor het andere object ook worden gewijzigd. Als een object dit geheugen verwijdert, zal de ander blijven proberen het te gebruiken - eek.

Om dit op te lossen, schrijft u uw eigen versie van de copy constructor en assignment-operator. Uw versies wijzen afzonderlijk geheugen toe aan de nieuwe objecten en kopiëren over de waarden waarnaar de eerste aanwijzer verwijst in plaats van het adres.


134
2018-05-14 14:22



Als je een destructor hebt (niet de standaard destructor), betekent dit dat de klasse die je hebt gedefinieerd, enige geheugentoewijzing heeft. Stel dat de klasse buiten wordt gebruikt door een bepaalde klantcode of door jou.

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

Als MyClass slechts enkele primitief getypeerde leden heeft, zou een standaardtoewijzingsoperator werken, maar als het pointerleden en objecten bevat die geen toewijzingsoperators hebben, is het resultaat onvoorspelbaar. Daarom kunnen we zeggen dat als er iets moet worden verwijderd in een destructor van een klasse, we mogelijk een deep-copy-operator nodig hebben, wat betekent dat we een copy-constructor en -toewijzingsoperator moeten bieden.


37
2017-12-31 19:29



Wat betekent het kopiëren van een voorwerp? Er zijn een paar manieren waarop u objecten kunt kopiëren - laten we het hebben over de 2 soorten waar u waarschijnlijk naar verwijst - diepgaande en ondiepe kopie.

Omdat we in een object-georiënteerde taal zijn (of op zijn minst aannemen), laten we zeggen dat je een stukje geheugen hebt toegewezen. Omdat het een OO-taal is, kunnen we eenvoudig verwijzen naar brokken geheugen die we toewijzen omdat het meestal primitieve variabelen (ints, chars, bytes) zijn of klassen die we hebben gedefinieerd en die zijn gemaakt van onze eigen typen en primitieven. Laten we zeggen dat we een klasse van auto's als volgt hebben:

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;
}

}

Een diepe kopie is als we een object declareren en vervolgens een volledig afzonderlijke kopie van het object maken ... we eindigen met 2 objecten in 2 volledige sets geheugen.

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.

Laten we nu iets vreemds doen. Laten we zeggen dat auto2 ofwel verkeerd is geprogrammeerd of met opzet is bedoeld om het werkelijke geheugen te delen waarvan car1 is gemaakt. (Het is meestal een vergissing om dit te doen en in klassen is dit meestal de deken die hieronder wordt besproken.) Stel je voor dat wanneer je maar vraagt ​​naar car2, je echt een pointer naar car1's geheugenruimte oplost ... dat is min of meer wat een oppervlakkige kopie is.

//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.*/

Dus ongeacht in welke taal je schrijft, wees heel voorzichtig met wat je bedoelt als het gaat om het kopiëren van objecten, omdat je meestal een diepe kopie wilt.

Wat zijn de copy-constructor en de copy-assignment-operator? Ik heb ze hierboven al gebruikt. De copy-constructor wordt aangeroepen wanneer u code typt zoals Car car2 = car1;  In wezen als u een variabele declareert en deze op één regel toewijst, wordt de constructeur van de kopie genoemd. De opdrachtoperator is wat er gebeurt als je een gelijkteken gebruikt--car2 = car1;. Merk op car2 wordt niet in dezelfde verklaring gedeclareerd. De twee stukjes code die u schrijft voor deze bewerkingen lijken waarschijnlijk erg op elkaar. In feite heeft het typische ontwerppatroon een andere functie die je roept om alles in te stellen zodra je tevreden bent dat de initiële kopie / toewijzing legitiem is - als je kijkt naar de lange-afstandscode die ik heb geschreven, zijn de functies bijna identiek.

Wanneer moet ik ze zelf aangeven? Als je geen code schrijft die op een of andere manier moet worden gedeeld of voor productie, moet je ze echt alleen declareren als je ze nodig hebt. U moet wel op de hoogte zijn van wat uw programmeertaal doet als u ervoor kiest om het 'per ongeluk' te gebruiken en er geen hebt gemaakt - d.w.z. je krijgt de compiler standaard. Ik gebruik bijvoorbeeld zelden copy-constructors, maar toewijzingen van operator-overrides zijn heel gewoon. Wist je dat je kunt overschrijven wat optellen, aftrekken, etc. ook betekent?

Hoe kan ik voorkomen dat mijn objecten worden gekopieerd? Overschrijven van alle manieren waarop u geheugen voor uw object met een privéfunctie kunt toewijzen, is een redelijke start. Als je echt niet wilt dat mensen ze kopiëren, kun je het openbaar maken en de programmeur waarschuwen door een uitzondering te maken en het object ook niet te kopiëren.


27
2017-10-17 16:37



Wanneer moet ik ze zelf aangeven?

De Regel van Drie stelt dat als je een van de volgende verklaart

  1. kopieer constructor
  2. kopieer opdracht operator
  3. destructor

dan moet je ze alle drie declareren. Het kwam voort uit de observatie dat de noodzaak om de betekenis van een kopieerbewerking over te nemen bijna altijd afkomstig was van het feit dat de klas een soort van resource management uitvoerde, en dat impliceerde bijna altijd dat

  • welk bronbeheer er in één kopieerbewerking werd gedaan, moest waarschijnlijk worden uitgevoerd in de andere kopieerbewerking en

  • de klassenvernietiger zou ook deelnemen aan het beheer van de hulpbron (meestal deze vrijgeven). De klassieke te beheren bron was het geheugen, en dit is de reden waarom alle klassen van de standaardbibliotheek dit zijn geheugen beheren (bijv. de STL-containers die dynamisch geheugenbeheer uitvoeren) verklaren allemaal "de grote drie": zowel kopieerbewerkingen als een destructor.

Een gevolg van de regel van drie is dat de aanwezigheid van een door de gebruiker gedeclareerde destructor aangeeft dat eenvoudige lidkopieën waarschijnlijk niet geschikt zijn voor de kopieerbewerkingen in de klas. Dat op zijn beurt suggereert dat als een klasse een destructor declareert, de kopieerbewerkingen waarschijnlijk niet automatisch gegenereerd moeten worden, omdat ze niet het goede zouden doen. Op het moment dat C ++ 98 werd aangenomen, werd de betekenis van deze redenering niet volledig gewaardeerd, dus in C ++ 98 had het bestaan ​​van een door een gebruiker gedeclareerde destructor geen invloed op de bereidheid van compilers om kopieerbewerkingen te genereren. Dat is nog steeds het geval in C ++ 11, maar alleen omdat het beperken van de omstandigheden waaronder de kopieerbewerkingen worden gegenereerd, te veel oude code zou doorbreken.

Hoe kan ik voorkomen dat mijn objecten worden gekopieerd?

Declareer copy constructor & copy assignment-operator als private access specifier.

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);
}

Vanaf C ++ 11 kunt u ook aangeven dat copy constructor & assignment-operator is verwijderd

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



Veel van de bestaande antwoorden raken de constructeur van de kopie, de toewijzingsoperator en de destructor al. In post C ++ 11 kan de introductie van bewegingssemantiek dit echter buiten 3 uitbreiden.

Onlangs gaf Michael Claisse een toespraak die dit onderwerp raakt: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class


9
2018-01-07 05:38



Regel van drie in C ++ is een fundamenteel principe van het ontwerp en de ontwikkeling van drie vereisten dat als er een duidelijke definitie is in een van de volgende lidfunctie, de programmeur de andere twee ledenfuncties samen moet definiëren. Namelijk de volgende drie lidfuncties zijn onmisbaar: destructor, kopie-constructor, kopie-toewijzingsoperator.

Copy constructor in C ++ is een speciale constructor. Het wordt gebruikt om een ​​nieuw object te bouwen, het nieuwe object dat equivalent is aan een kopie van een bestaand object.

Copy assignment operator is een speciale opdrachtoperator die meestal wordt gebruikt om een ​​bestaand object op te geven aan anderen van hetzelfde type object.

Er zijn snelle voorbeelden:

// 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