Jaka jest reguła trzech?

2145
  • Co oznacza kopiowanie obiektu ?
  • Co to jest konstruktor kopii i operator przypisania kopii ?
  • Kiedy sam muszę je zadeklarować?
  • Jak mogę zapobiec kopiowaniu moich obiektów?
fredoverflow
źródło
52
Proszę przeczytać cały ten wątek i na c++-faqwiki tag zanim głosować zamknąć .
sbi
13
@Binary: Przynajmniej poświęć trochę czasu na przeczytanie dyskusji na temat komentarzy przed oddaniem głosu. Kiedyś tekst był znacznie prostszy, ale Fred został poproszony o rozwinięcie go. Ponadto, choć gramatycznie są to cztery pytania , tak naprawdę jest to tylko jedno pytanie z kilkoma aspektami. (Jeśli nie zgadzasz się z tym, to udowodnij swój POV, odpowiadając na każde z tych pytań osobno i pozwól nam głosować nad wynikami.)
sbi
1
Fred, oto interesujący dodatek do twojej odpowiedzi dotyczącej C ++ 1x: stackoverflow.com/questions/4782757/… . Jak sobie z tym poradzić?
sbi
6
Powiązane: Prawo dwóch wielkich
Nemanja Trifunovic
4
Należy pamiętać, że od wersji C ++ 11 myślę, że została zaktualizowana do reguły pięciu lub coś w tym rodzaju.
paxdiablo,

Odpowiedzi:

1793

Wprowadzenie

C ++ traktuje zmienne typów zdefiniowanych przez użytkownika z semantyką wartości . Oznacza to, że obiekty są domyślnie kopiowane w różnych kontekstach i powinniśmy zrozumieć, co tak naprawdę oznacza „kopiowanie obiektu”.

Rozważmy prosty przykład:

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

(Jeśli zastanawia Cię ta name(name), age(age)część, nazywa się to listą inicjującą członka ).

Specjalne funkcje członka

Co to znaczy skopiować personobiekt? Ta mainfunkcja pokazuje dwa różne scenariusze kopiowania. Inicjalizacja person b(a);jest wykonywana przez konstruktor kopii . Jego zadaniem jest konstruowanie świeżego obiektu na podstawie stanu istniejącego obiektu. Przypisanie b = ajest wykonywane przez operatora przypisania kopii . Jego zadanie jest na ogół nieco bardziej skomplikowane, ponieważ obiekt docelowy jest już w pewnym prawidłowym stanie, z którym należy sobie poradzić.

Ponieważ sami nie zadeklarowaliśmy ani konstruktora kopiowania, ani operatora przypisania (ani destruktora), są one dla nas domyślnie zdefiniowane. Cytat ze standardu:

Konstruktor [...] kopii i operator przypisania kopii, [...] i destruktor są specjalnymi funkcjami składowymi. [ Uwaga : Implementacja domyślnie zadeklaruje te funkcje składowe dla niektórych typów klas, gdy program ich jawnie nie zadeklaruje. Implementacja domyślnie je zdefiniuje, jeśli zostaną użyte. [...] uwaga końcowa ] [n3126.pdf sekcja 12 §1]

Domyślnie kopiowanie obiektu oznacza kopiowanie jego elementów:

Niejawnie zdefiniowany konstruktor kopii dla nieunijnej klasy X wykonuje członkową kopię swoich podobiektów. [n3126.pdf sekcja 12.8 §16]

Niejawnie zdefiniowany operator przypisania kopii dla nieunijnej klasy X wykonuje przypisanie członkowe swoich podobiektów. [n3126.pdf sekcja 12.8 §30]

Domniemane definicje

Domyślnie zdefiniowane specjalne funkcje personskładowe dla wyglądają następująco:

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

Kopiowanie według członków jest dokładnie tym, czego chcemy w tym przypadku: namei agesą kopiowane, więc otrzymujemy niezależny, niezależny personobiekt. Domyślnie zdefiniowany destruktor jest zawsze pusty. Jest to również w porządku w tym przypadku, ponieważ nie uzyskaliśmy żadnych zasobów w konstruktorze. Destruktory członków są domyślnie wywoływane po zakończeniu persondestruktora:

Po wykonaniu korpusu destruktora i zniszczeniu wszelkich automatycznych obiektów przydzielonych w ciele, destruktor klasy X wywołuje destruktory dla bezpośrednich członków X [...] [n3126.pdf 12.4 §6]

Zarządzanie zasobami

Kiedy więc powinniśmy wyraźnie zadeklarować te specjalne funkcje członka? Kiedy nasza klasa zarządza zasobem , to znaczy, gdy obiekt klasy jest odpowiedzialny za ten zasób. Zazwyczaj oznacza to, że zasób jest nabywany w konstruktorze (lub przekazywany do konstruktora) i uwalniany w destruktorze.

Cofnijmy się w czasie do wstępnie standardowego C ++. Nie było czegoś takiego std::string, a programiści byli zakochani we wskaźnikach. personKlasa mógł wyglądać tak:

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

Nawet dzisiaj ludzie nadal piszą zajęcia w tym stylu i wpadają w kłopoty: „ Zepchnąłem osobę na wektor, a teraz dostaję szalonych błędów pamięci! ” Pamiętaj, że domyślnie kopiowanie obiektu oznacza kopiowanie jego elementów, ale kopiowanie nametylko elementu kopiuje wskaźnik, a nie tablicę znaków, na którą wskazuje! Ma to kilka nieprzyjemnych efektów:

  1. Zmiany za pośrednictwem amożna obserwować za pomocą b.
  2. Raz bzniszczony, a.namezwisający wskaźnik.
  3. Jeśli azostanie zniszczony, usunięcie zwisającego wskaźnika daje niezdefiniowane zachowanie .
  4. Ponieważ przydział nie bierze pod uwagę tego, na co namewskazywał przed przydziałem, prędzej czy później w całym miejscu pojawią się wycieki pamięci.

Jawne definicje

Ponieważ kopiowanie członków nie daje pożądanego efektu, musimy jawnie zdefiniować konstruktor kopii i operator przypisania kopii, aby wykonać głębokie kopie tablicy znaków:

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

Zwróć uwagę na różnicę między inicjalizacją a przypisaniem: musimy rozebrać stary stan przed przypisaniem, nameaby zapobiec wyciekom pamięci. Musimy również chronić przed samodzielnym przypisaniem formularza x = x. Bez tej kontroli, delete[] nameby usunąć tablicę zawierającą źródłowy łańcuch, bo kiedy piszesz x = x, zarówno this->namei that.namezawierają ten sam wskaźnik.

Wyjątkowe bezpieczeństwo

Niestety, to rozwiązanie zawiedzie, jeśli new char[...]zgłosi wyjątek z powodu wyczerpania pamięci. Jednym z możliwych rozwiązań jest wprowadzenie zmiennej lokalnej i zmiana kolejności instrukcji:

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

Dotyczy to również samodzielnego przydzielania bez wyraźnej kontroli. Jeszcze bardziej niezawodnym rozwiązaniem tego problemu jest idiom kopiowania i zamiany , ale nie będę tu wchodził w szczegóły dotyczące wyjątkowego bezpieczeństwa. Wspomniałem tylko o wyjątkach, aby przedstawić następujący punkt: Pisanie klas zarządzających zasobami jest trudne.

Zasoby, których nie można kopiować

Niektórych zasobów nie można lub nie należy kopiować, takich jak uchwyty plików lub muteksy. W takim przypadku po prostu zadeklaruj konstruktor kopiowania i operator przypisania kopii jako privatebez podania definicji:

private:

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

Możesz też dziedziczyć je boost::noncopyablelub zadeklarować jako usunięte (w C ++ 11 i nowszych):

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

Zasada trzech

Czasami musisz zaimplementować klasę zarządzającą zasobem. (Nigdy nie zarządzaj wieloma zasobami w jednej klasie, spowoduje to tylko ból.) W takim przypadku pamiętaj o zasadzie trzech :

Jeśli musisz samodzielnie zadeklarować destruktor, konstruktor kopii lub operator przypisania kopii, prawdopodobnie musisz jawnie zadeklarować wszystkie trzy z nich.

(Niestety, ta „reguła” nie jest egzekwowana przez standard C ++ ani żaden kompilator, o którym wiem.)

Zasada pięciu

Począwszy od C ++ 11 obiekt ma 2 dodatkowe specjalne funkcje składowe: konstruktor ruchu i przypisanie ruchu. Zasada pięciu stanów, aby również wprowadzić te funkcje.

Przykład z podpisami:

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

Reguła zerowa

Reguła 3/5 jest również określana jako reguła 0/3/5. Zerowa część reguły stanowi, że nie wolno pisać żadnych specjalnych funkcji składowych podczas tworzenia klasy.

Rada

Przez większość czasu nie musisz samodzielnie zarządzać zasobem, ponieważ istniejąca klasa, taka jak std::stringjuż, robi to za Ciebie. Wystarczy porównać prosty kod za pomocą std::stringelementu członkowskiego ze skomplikowaną i podatną na błędy alternatywą za pomocą a char*i powinieneś być przekonany. Tak długo, jak trzymasz się z daleka od surowych elementów wskaźnika, jest mało prawdopodobne, aby zasada trzech dotyczyła twojego własnego kodu.

fredoverflow
źródło
4
Fred, czułbym się lepiej z moim głosowaniem na górę, jeśli (A) nie przeliterujesz źle zaimplementowanego zadania w kodzie do kopiowania i dodasz notatkę z informacją, że jest źle, i spójrz gdzie indziej na drobny druk; albo użyj c & s w kodzie, albo po prostu pomiń implementację wszystkich tych elementów (B), skróciłbyś pierwszą połowę, co ma niewiele wspólnego z RoT; (C) omówiłbyś wprowadzenie semantyki ruchu i co to oznacza dla RoT.
sbi
7
Ale myślę, że wtedy post powinien być napisany jako C / W. Podoba mi się, że trzymasz się terminów w większości dokładnych (tzn. Mówisz „ operator kopiowania przypisań” i że nie wpadasz w powszechną pułapkę, że przypisanie nie może oznaczać kopii).
Johannes Schaub - litb
4
@Prasoon: Nie sądzę, że wycięcie połowy odpowiedzi byłoby postrzegane jako „uczciwa edycja” odpowiedzi spoza CW.
sbi
69
Byłoby wspaniale, gdybyś zaktualizował swój post dla C ++ 11 (tj. Przeniesienie konstruktora / zadania)
Alexander Malakhov,
5
@solalito Wszystko, co musisz wydać po użyciu: blokady współbieżności, uchwyty plików, połączenia z bazą danych, gniazda sieciowe, pamięć sterty ...
fredoverflow
509

Reguła Trzech jest zasada dla C ++, w zasadzie mówi

Jeśli twoja klasa potrzebuje czegoś

  • konstruktor kopiujący ,
  • operatorowi przypisanie ,
  • lub destruktor ,

zdefiniowane dokładnie, wtedy prawdopodobnie będą potrzebować wszystkich trzech .

Powodem tego jest to, że wszystkie trzy są zwykle używane do zarządzania zasobem, a jeśli klasa zarządza zasobem, zwykle musi zarządzać zarówno kopiowaniem, jak i zwalnianiem.

Jeśli nie ma dobrego semantycznego narzędzia do kopiowania zasobu, którym zarządza klasa, rozważ zakaz zabrania się kopiowania, deklarując (nie definiując ) konstruktora kopiowania i operatora przypisania jako private.

(Zauważ, że nadchodząca nowa wersja standardu C ++ (czyli C ++ 11) dodaje semantykę przenoszenia do C ++, co prawdopodobnie zmieni Regułę Trzech. Jednak wiem za mało o tym, aby napisać sekcję C ++ 11 o zasadzie trzech).

sbi
źródło
3
Innym rozwiązaniem zapobiegającym kopiowaniu jest dziedziczenie (prywatnie) od klasy, której nie można skopiować (jak boost::noncopyable). Może być również znacznie jaśniejszy. Myślę, że C ++ 0x i możliwość „usuwania” funkcji mogłyby tu pomóc, ale zapomniałem o składni: /
Matthieu M.
2
@Matthieu: Tak, to też działa. Ale chyba, że noncopyablejest częścią standardowej biblioteki, nie uważam tego za poprawę. (Aha, a jeśli zapomniałeś o składni usuwania, zapomniałeś o moim :)
etanie,
3
@Daan: Zobacz tę odpowiedź . Jednakże, polecam trzymać się Martinho „s Reguły Zero . Dla mnie jest to jedna z najważniejszych praktycznych zasad dla C ++ wymyślonych w ostatniej dekadzie.
sbi
3
Reguła zerowa Martinho teraz lepsza (bez widocznego przejęcia adware) znajdująca się na archive.org
Nathan Kidd
161

Prawo wielkiej trójki jest określone powyżej.

Prosty przykład, w prostym języku angielskim, rodzaju problemu, który rozwiązuje:

Niszczyciel inny niż domyślny

Przydzieliłeś pamięć do swojego konstruktora, więc musisz napisać destruktor, aby ją usunąć. W przeciwnym razie spowodujesz wyciek pamięci.

Możesz pomyśleć, że to robota wykonana.

Problem będzie polegał na tym, że jeśli twoja kopia zostanie wykonana z twojego obiektu, wówczas kopia wskaże tę samą pamięć co oryginalny obiekt.

Raz jeden z nich usuwa pamięć z niszczyciela, drugi będzie miał wskaźnik do nieprawidłowej pamięci (zwany to zwisającym wskaźnikiem), gdy spróbuje go użyć, rzeczy staną się owłosione.

Dlatego piszesz konstruktor kopii, aby przydzielał nowym obiektom własne fragmenty pamięci do zniszczenia.

Operator przypisania i konstruktor kopii

Przydzieliłeś pamięć w swoim konstruktorze wskaźnikowi członkowskiemu twojej klasy. Podczas kopiowania obiektu tej klasy domyślny operator przypisania i konstruktor kopiowania skopiują wartość tego wskaźnika elementu do nowego obiektu.

Oznacza to, że nowy obiekt i stary obiekt będą wskazywały na ten sam fragment pamięci, więc kiedy zmienisz go w jednym obiekcie, zostanie on również zmieniony na inny obiekt. Jeśli jeden obiekt usunie tę pamięć, drugi będzie próbował z niej skorzystać - np.

Aby rozwiązać ten problem, napisz własną wersję konstruktora kopiowania i operatora przypisania. Twoje wersje przydzielają osobną pamięć nowym obiektom i kopiują wartości, które wskazuje pierwszy wskaźnik, a nie jego adres.

Stefan
źródło
4
Jeśli więc użyjemy konstruktora kopiowania, kopia zostanie wykonana, ale w zupełnie innej lokalizacji pamięci, a jeśli nie użyjemy konstruktora kopii, kopia zostanie wykonana, ale wskazuje na tę samą lokalizację pamięci. czy to właśnie próbujesz powiedzieć? Tak więc kopia bez konstruktora kopiowania oznacza, że ​​będzie tam nowy wskaźnik, ale wskazuje na to samo miejsce w pamięci, jednak jeśli mamy wyraźnie zdefiniowany przez użytkownika konstruktor kopii, będziemy mieli oddzielny wskaźnik wskazujący inną lokalizację pamięci, ale posiadającą dane.
Niezniszczalny
4
Niestety, ja odpowiedziały na to wieki temu, ale moja odpowiedź nie wydaje się być jeszcze tutaj :-( W zasadzie tak - masz go :-)
Stefan
1
Jak ta zasada pasuje do operatora przypisania kopii? Ta odpowiedź byłaby bardziej przydatna, gdyby wspomniano o trzeciej z reguły trzech.
DBedrenko
1
@DBedrenko, „piszesz konstruktor kopii, aby przydzielał nowym obiektom własne fragmenty pamięci ...” ta sama zasada dotyczy operatora przypisywania kopii. Czy nie sądzisz, że to wyjaśniłem?
Stefan
2
@DBedrenko, dodałem więcej informacji. Czy to czyni to jaśniejszym?
Stefan
44

Zasadniczo, jeśli masz destruktor (nie domyślny destruktor), oznacza to, że zdefiniowana klasa ma pewien przydział pamięci. Załóżmy, że klasa jest używana na zewnątrz przez jakiś kod klienta lub przez ciebie.

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

Jeśli MyClass ma tylko niektóre prymitywne elementy o typie, działałby domyślny operator przypisania, ale jeśli ma on pewne elementy wskaźnikowe i obiekty, które nie mają operatorów przypisania, wynik byłby nieprzewidywalny. Dlatego możemy powiedzieć, że jeśli coś jest do usunięcia w destruktorze klasy, możemy potrzebować operatora głębokiego kopiowania, co oznacza, że ​​powinniśmy udostępnić konstruktor kopiujący i operator przypisania.

fatma.ekici
źródło
36

Co oznacza kopiowanie obiektu? Istnieje kilka sposobów kopiowania obiektów - porozmawiajmy o 2 rodzajach, do których najprawdopodobniej masz na myśli - głębokiej i płytkiej kopii.

Ponieważ jesteśmy w języku zorientowanym obiektowo (lub przynajmniej tak zakładamy), powiedzmy, że masz przydzieloną pamięć. Ponieważ jest to język OO, możemy z łatwością odwoływać się do przydzielanych przez nas fragmentów pamięci, ponieważ są to zwykle prymitywne zmienne (int, znaki, bajty) lub zdefiniowane przez nas klasy, które są zbudowane z naszych własnych typów i prymitywów. Powiedzmy, że mamy klasę samochodów w następujący sposób:

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

}

Głęboka kopia ma miejsce, gdy zadeklarujemy obiekt, a następnie utworzymy całkowicie oddzielną kopię obiektu ... otrzymamy 2 obiekty w 2 kompletach pamięci.

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.

Zróbmy teraz coś dziwnego. Powiedzmy, że car2 jest źle zaprogramowany lub celowo ma na celu współdzielenie faktycznej pamięci, z której składa się car1. (Zazwyczaj jest to pomyłka, a na zajęciach zwykle jest to koc, o którym mowa.) Udawaj, że za każdym razem, gdy pytasz o car2, naprawdę rozwiązujesz wskaźnik do przestrzeni pamięci car1 ... to mniej więcej taka płytka kopia jest.

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

Bez względu na to, w jakim języku piszesz, bądź bardzo ostrożny, jeśli chodzi o kopiowanie obiektów, ponieważ przez większość czasu potrzebujesz głębokiej kopii.

Co to jest konstruktor kopii i operator przypisania kopii? Użyłem ich już powyżej. Konstruktor kopiowania jest wywoływany podczas wpisywania kodu, takiego jak Car car2 = car1; Zasadniczo, jeśli deklarujesz zmienną i przypisujesz ją w jednym wierszu, wtedy wywoływany jest konstruktor kopiowania. Operator przypisania jest tym, co dzieje się, gdy używasz znaku równości-- car2 = car1;. Powiadomienie car2nie jest zadeklarowane w tym samym oświadczeniu. Dwie części kodu, które piszesz dla tych operacji, są prawdopodobnie bardzo podobne. W rzeczywistości typowy wzorzec projektowy ma inną funkcję, którą wywołujesz, aby ustawić wszystko, gdy jesteś zadowolony, że początkowe kopiowanie / przypisanie jest uzasadnione - jeśli spojrzysz na napisany odręcznie kod, funkcje są prawie identyczne.

Kiedy sam muszę je zadeklarować? Jeśli nie piszesz kodu, który ma być w jakiś sposób udostępniany lub produkowany, naprawdę musisz go zadeklarować tylko wtedy, gdy jest potrzebny. Musisz zdawać sobie sprawę z tego, co robi Twój język programu, jeśli zdecydujesz się go użyć „przypadkowo”, ale go nie utworzyłeś - tzn. Otrzymasz domyślny kompilator. Na przykład rzadko używam konstruktorów kopiowania, ale przesłonięcia operatora przypisania są bardzo częste. Czy wiesz, że możesz pominąć znaczenie dodawania, odejmowania itp.?

Jak mogę zapobiec kopiowaniu moich obiektów? Zastąpienie wszystkich sposobów przydzielania pamięci dla obiektu za pomocą funkcji prywatnej jest rozsądnym początkiem. Jeśli naprawdę nie chcesz, aby ludzie je kopiowali, możesz upublicznić je i ostrzec programistę, zgłaszając wyjątek, a także nie kopiując obiektu.

użytkownik1701047
źródło
5
Pytanie oznaczono jako C ++. Ta ekspozycja pseudokodu niewiele robi, by wyjaśnić cokolwiek na temat dobrze zdefiniowanej „Reguły trzech”, a w najgorszym przypadku wprowadza zamieszanie.
patrz
26

Kiedy sam muszę je zadeklarować?

Reguła Trzech stanowi, że jeśli zadeklarujesz którykolwiek z

  1. konstruktor kopii
  2. operator przypisania kopii
  3. burzyciel

wtedy powinieneś zadeklarować wszystkie trzy. Wyrosło z obserwacji, że potrzeba przejęcia znaczenia operacji kopiowania prawie zawsze wynikała z klasy wykonującej pewnego rodzaju zarządzanie zasobami, a to prawie zawsze implikowało, że

  • cokolwiek zarządzanie zasobami było wykonywane w jednej operacji kopiowania, prawdopodobnie musiało być wykonane w drugiej operacji kopiowania i

  • niszczyciel klas uczestniczyłby również w zarządzaniu zasobem (zwykle zwalniając go). Klasycznym zasobem do zarządzania była pamięć i dlatego wszystkie klasy Biblioteki Standardowej, które zarządzają pamięcią (np. Kontenery STL, które wykonują dynamiczne zarządzanie pamięcią) wszystkie deklarują „wielką trójkę”: zarówno operacje kopiowania, jak i destruktor.

Konsekwencją reguły trzech jest to, że obecność destruktora zadeklarowanego przez użytkownika wskazuje, że prosta kopia mądrego elementu nie jest odpowiednia do operacji kopiowania w klasie. To z kolei sugeruje, że jeśli klasa zadeklaruje destruktor, operacje kopiowania prawdopodobnie nie powinny być generowane automatycznie, ponieważ nie zrobiłyby właściwej rzeczy. W momencie przyjęcia C ++ 98 znaczenie tego rozumowania nie zostało w pełni docenione, dlatego w C ++ 98 istnienie deklarowanego przez użytkownika destruktora nie miało wpływu na gotowość kompilatorów do generowania operacji kopiowania. Tak jest nadal w C ++ 11, ale tylko dlatego, że ograniczenie warunków, w których generowane są operacje kopiowania, spowodowałoby uszkodzenie zbyt dużej ilości starszego kodu.

Jak mogę zapobiec kopiowaniu moich obiektów?

Zadeklaruj konstruktora kopiowania i operatora przypisania kopii jako prywatny specyfikator dostępu.

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

W wersji C ++ 11 możesz także zadeklarować usunięcie konstruktora i operatora przypisania

class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};


int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}
Ajay Yadav
źródło
10

Zasada trzech w C ++ jest podstawową zasadą projektowania i rozwijania trzech wymagań, że jeśli istnieje jednoznaczna definicja jednej z poniższych funkcji składowych, wówczas programista powinien zdefiniować funkcje pozostałych dwóch składowych razem. Niezbędne są następujące trzy funkcje składowe: destruktor, konstruktor kopii, operator przypisania kopii.

Konstruktor kopii w C ++ jest specjalnym konstruktorem. Służy do budowy nowego obiektu, który jest nowym obiektem równoważnym z kopią istniejącego obiektu.

Kopiuj operator przypisania jest specjalnym operatorem przypisania, który zwykle służy do określania istniejącego obiektu innym obiektom tego samego typu.

Istnieją szybkie przykłady:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;
Marcus Thornton
źródło
7
Cześć, twoja odpowiedź nie dodaje nic nowego. Inne poruszają ten temat na znacznie większej głębokości, a dokładniej - twoja odpowiedź jest przybliżona i w rzeczywistości jest błędna w niektórych miejscach (mianowicie nie ma tu „musi”, ale „najprawdopodobniej powinna”). Naprawdę nie opłaca się pisać tego rodzaju odpowiedzi na pytania, na które już udzielono dokładnych odpowiedzi. Chyba że masz nowe rzeczy do dodania.
Mat
1
Są też cztery szybkie przykłady, które są w jakiś sposób powiązane z dwoma z trzech , o których mówi Zasada Trzech. Za dużo zamieszania.
anatolyg