Kiedy musimy używać konstruktorów kopiujących?


87

Wiem, że kompilator C ++ tworzy konstruktor kopiujący dla klasy. W takim przypadku musimy napisać konstruktor kopiujący zdefiniowany przez użytkownika? Czy możesz podać kilka przykładów?



1
Jeden z przypadków, w których należy napisać własnego kopiownika: Kiedy musisz wykonać głębokie kopiowanie. Zauważ również, że gdy tylko utworzysz ctor, nie zostanie utworzony żaden domyślny ctor (chyba że użyjesz domyślnego słowa kluczowego).
harshvchawla

Odpowiedzi:


75

Konstruktor kopiujący wygenerowany przez kompilator wykonuje kopiowanie według elementów członkowskich. Czasami to nie wystarcza. Na przykład:

class Class {
public:
    Class( const char* str );
    ~Class();
private:
    char* stored;
};

Class::Class( const char* str )
{
    stored = new char[srtlen( str ) + 1 ];
    strcpy( stored, str );
}

Class::~Class()
{
    delete[] stored;
}

w tym przypadku kopiowanie storedelementu składowego nie spowoduje zduplikowania bufora (skopiowany zostanie tylko wskaźnik), więc pierwsza zniszczona kopia udostępniająca bufor zostanie delete[]pomyślnie wywołana, a druga będzie działać w niezdefiniowanym zachowaniu. Potrzebujesz konstruktora kopiującego do głębokiego kopiowania (oraz operatora przypisania).

Class::Class( const Class& another )
{
    stored = new char[strlen(another.stored) + 1];
    strcpy( stored, another.stored );
}

void Class::operator = ( const Class& another )
{
    char* temp = new char[strlen(another.stored) + 1];
    strcpy( temp, another.stored);
    delete[] stored;
    stored = temp;
}

10
Nie wykonuje kopiowania bitowego, ale kopiowania opartego na elementach, które w szczególności wywołuje kopiowanie dla elementów członkowskich typu.
Georg Fritzsche,

7
Nie pisz w ten sposób operatora przypisania. To nie jest wyjątkowo bezpieczne. (jeśli nowy zgłasza wyjątek, obiekt pozostaje w niezdefiniowanym stanie z magazynem wskazującym na zwolnioną część pamięci (zwalniaj pamięć TYLKO po pomyślnym zakończeniu wszystkich operacji, które mogą zostać wyrzucone)). Prostym rozwiązaniem jest użycie idium do wymiany kopii.
Martin York,

@sharptooth Trzecia linia od dołu, którą masz delete stored[];i uważam, że powinno byćdelete [] stored;
Peter Ajtai.

4
Wiem, że to tylko przykład, ale powinieneś wskazać, że lepszym rozwiązaniem jest użycie std::string. Ogólna idea jest taka, że ​​tylko klasy narzędziowe, które zarządzają zasobami, muszą przeciążać Wielką Trójkę, a wszystkie inne klasy powinny po prostu używać tych klas narzędzi, eliminując potrzebę definiowania którejkolwiek z Wielkiej Trójki.
GManNickG,

2
@Martin: Chciałem się upewnić, że został wyryty w kamieniu. : P
GManNickG

46

Trochę się denerwuję, że Rule of Fivenie przytoczono zasady z .

Ta zasada jest bardzo prosta:

Zasada pięciu : za
każdym razem, gdy piszesz jeden z Destructor, Copy Constructor, Copy Assignment Operator, Move Constructor lub Move Assignment Operator, prawdopodobnie będziesz musiał napisać pozostałe cztery.

Jest jednak bardziej ogólna wskazówka, której należy przestrzegać, która wynika z potrzeby pisania kodu bezpiecznego dla wyjątków:

Każdy zasób powinien być zarządzany przez dedykowany obiekt

Tutaj @sharptoothkod jest nadal (w większości) w porządku, jednak gdyby miał dodać drugi atrybut do swojej klasy, to by tak nie było. Rozważ następującą klasę:

class Erroneous
{
public:
  Erroneous();
  // ... others
private:
  Foo* mFoo;
  Bar* mBar;
};

Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}

Co się stanie, jeśli new Barrzuci? Jak usunąć wskazany obiekt mFoo? Istnieją rozwiązania (try / catch ...), po prostu się nie skalują.

Właściwym sposobem radzenia sobie z tą sytuacją jest użycie odpowiednich klas zamiast surowych wskaźników.

class Righteous
{
public:
private:
  std::unique_ptr<Foo> mFoo;
  std::unique_ptr<Bar> mBar;
};

Dzięki tej samej implementacji konstruktora (a właściwie używaniu make_unique) mam teraz wyjątek bezpieczeństwa za darmo !!! Czy to nie jest ekscytujące? A co najważniejsze, nie muszę już martwić się o odpowiedni destruktor! Muszę napisać własne Copy Constructori Assignment Operatorchoć, bo unique_ptrnie definiuje tych operacji ... ale to nie ma znaczenia;)

Dlatego sharptoothponownie odwiedzono klasę:

class Class
{
public:
  Class(char const* str): mData(str) {}
private:
  std::string mData;
};

Nie wiem jak Ty, ale moje jest dla mnie łatwiejsze;)


Dla C ++ 11 - reguła pięciu, która dodaje do reguły trójki konstruktor ruchu i operator przypisania ruchu.
Robert Andrzejuk

1
@Robb: Zauważ, że tak naprawdę, jak pokazano w ostatnim przykładzie, powinieneś ogólnie dążyć do zasady zera . Tylko wyspecjalizowane (ogólne) klasy techniczne powinny zajmować się obsługą jednego zasobu, wszystkie inne klasy powinny używać tych inteligentnych wskaźników / kontenerów i nie martwić się o to.
Matthieu M.

@MatthieuM. Zgoda :-) Wspomniałem o regule pięciu, ponieważ ta odpowiedź jest przed C ++ 11 i zaczyna się od „wielkiej trójki”, ale należy wspomnieć, że teraz „wielka piątka” ma znaczenie. Nie chcę odrzucać tej odpowiedzi, ponieważ jest ona poprawna w zadanym kontekście.
Robert Andrzejuk

@Robb: Słuszna uwaga, zaktualizowałem odpowiedź, aby wspomnieć o zasadzie pięciu zamiast wielkiej trójki. Miejmy nadzieję, że większość ludzi przeszła już do kompilatorów obsługujących C ++ 11 (i szkoda tych, którzy jeszcze tego nie robili).
Matthieu M.

31

Mogę przypomnieć sobie z mojej praktyki i pomyśleć o następujących przypadkach, w których mamy do czynienia z jawnym zadeklarowaniem / zdefiniowaniem konstruktora kopiującego. Pogrupowałem sprawy w dwie kategorie

  • Poprawność / semantyka - jeśli nie podasz konstruktora kopiującego zdefiniowanego przez użytkownika, programy używające tego typu mogą się nie skompilować lub mogą działać nieprawidłowo.
  • Optymalizacja - zapewnienie dobrej alternatywy dla konstruktora kopiującego generowanego przez kompilator pozwala przyspieszyć działanie programu.


Poprawność / semantyka

W tej sekcji umieszczam przypadki, w których zadeklarowanie / zdefiniowanie konstruktora kopiującego jest niezbędne do poprawnego działania programów używających tego typu.

Po przeczytaniu tej sekcji dowiesz się o kilku pułapkach związanych z zezwoleniem kompilatorowi na samodzielne wygenerowanie konstruktora kopiującego. Dlatego, jak zauważył Seand w swojej odpowiedzi , zawsze można bezpiecznie wyłączyć kopiowalność dla nowej klasy i celowo włączyć ją później, gdy jest to naprawdę potrzebne.

Jak sprawić, by klasa nie była kopiowalna w C ++ 03

Zadeklaruj prywatny konstruktor kopiujący i nie dostarczaj dla niego implementacji (aby kompilacja nie powiodła się na etapie łączenia, nawet jeśli obiekty tego typu są kopiowane we własnym zakresie klasy lub przez jej znajomych).

Jak sprawić, by klasa nie była kopiowalna w C ++ 11 lub nowszym

Zadeklaruj konstruktora kopiującego z =deleteat end.


Shallow vs Deep Copy

Jest to najlepiej zrozumiany przypadek i właściwie jedyny wymieniony w innych odpowiedziach. shaprtooth został pokryty go całkiem dobrze. Chcę tylko dodać, że głębokie kopiowanie zasobów, które powinny być wyłączną własnością obiektu, może dotyczyć dowolnego typu zasobów, z których dynamicznie przydzielana pamięć jest tylko jednym rodzajem. W razie potrzeby może również wymagać głębokiego skopiowania obiektu

  • kopiowanie plików tymczasowych na dysk
  • otwarcie oddzielnego połączenia sieciowego
  • tworzenie oddzielnego wątku roboczego
  • przydzielanie oddzielnego bufora ramki OpenGL
  • itp

Obiekty samorejestracyjne

Rozważ klasę, w której wszystkie obiekty - bez względu na to, jak zostały zbudowane - MUSZĄ zostać w jakiś sposób zarejestrowane. Kilka przykładów:

  • Najprostszy przykład: utrzymywanie całkowitej liczby aktualnie istniejących obiektów. Rejestracja obiektu polega tylko na zwiększaniu licznika statycznego.

  • Bardziej złożonym przykładem jest pojedynczy rejestr, w którym przechowywane są odwołania do wszystkich istniejących obiektów tego typu (tak, aby powiadomienia mogły być dostarczane do wszystkich z nich).

  • Inteligentne wskaźniki liczone jako referencje można uznać za szczególny przypadek w tej kategorii: nowy wskaźnik „rejestruje się” we współdzielonym zasobie, a nie w rejestrze globalnym.

Taka operacja samodzielnej rejestracji musi zostać wykonana przez DOWOLNEGO konstruktora typu, a konstruktor kopiujący nie jest wyjątkiem.


Obiekty z wewnętrznymi odsyłaczami

Niektóre obiekty mogą mieć nietrywialną strukturę wewnętrzną z bezpośrednimi odsyłaczami między ich różnymi podobiektami (w rzeczywistości wystarczy jeden taki wewnętrzny odsyłacz, aby wywołać ten przypadek). Konstruktor kopiujący dostarczony przez kompilator przerwie wewnętrzne powiązania między obiektami , konwertując je na skojarzenia między obiektami .

Przykład:

struct MarriedMan;
struct MarriedWoman;

struct MarriedMan {
    // ...
    MarriedWoman* wife;   // association
};

struct MarriedWoman {
    // ...
    MarriedMan* husband;  // association
};

struct MarriedCouple {
    MarriedWoman wife;    // aggregation
    MarriedMan   husband; // aggregation

    MarriedCouple() {
        wife.husband = &husband;
        husband.wife = &wife;
    }
};

MarriedCouple couple1; // couple1.wife and couple1.husband are spouses

MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?

Kopiowane mogą być tylko obiekty spełniające określone kryteria

Mogą istnieć klasy, w których obiekty można bezpiecznie kopiować w jakimś stanie (np. Domyślny stan skonstruowany) i nie można ich kopiować w inny sposób. Jeśli chcemy zezwolić na kopiowanie obiektów bezpiecznych do kopiowania, to - jeśli programujemy defensywnie - potrzebujemy sprawdzenia w czasie wykonywania w konstruktorze kopiującym zdefiniowanym przez użytkownika.


Podobiekty nie do kopiowania

Czasami klasa, która powinna być kopiowalna, agreguje niepodlegające kopiowaniu podobiekty. Zwykle dzieje się tak w przypadku obiektów ze stanem nieobserwowalnym (ten przypadek jest omówiony bardziej szczegółowo w sekcji „Optymalizacja” poniżej). Kompilator jedynie pomaga rozpoznać ten przypadek.


Quasi-kopiowalne podobiekty

Klasa, która powinna być kopiowalna, może agregować podobiekt typu quasi-kopiowalnego. Typ quasi-kopiowalny nie zapewnia konstruktora kopiującego w ścisłym tego słowa znaczeniu, ale ma inny konstruktor, który pozwala na utworzenie koncepcyjnej kopii obiektu. Przyczyną tworzenia typu quasi-kopiowalnego jest brak pełnej zgody co do semantyki kopiowania typu.

Na przykład, ponownie analizując przypadek samodzielnej rejestracji obiektu, możemy argumentować, że mogą wystąpić sytuacje, w których obiekt musi być zarejestrowany w globalnym menedżerze obiektów tylko wtedy, gdy jest to kompletny samodzielny obiekt. Jeśli jest to podobiekt innego obiektu, odpowiedzialność za zarządzanie nim spoczywa na obiekcie zawierającym go.

Lub musi być obsługiwane zarówno płytkie, jak i głębokie kopiowanie (żadne z nich nie jest domyślne).

Ostateczna decyzja należy wtedy do użytkowników tego typu - kopiując obiekty, muszą oni jednoznacznie określić (poprzez dodatkowe argumenty) zamierzony sposób kopiowania.

W przypadku nieobronnego podejścia do programowania możliwe jest również, że obecny jest zarówno zwykły konstruktor kopiujący, jak i konstruktor quasi-kopiujący. Może to być uzasadnione, gdy w zdecydowanej większości przypadków należy zastosować jedną metodę kopiowania, aw rzadkich, ale dobrze zrozumiałych sytuacjach, należy zastosować alternatywne metody kopiowania. Wówczas kompilator nie będzie narzekał, że nie może niejawnie zdefiniować konstruktora kopiującego; wyłączną odpowiedzialnością użytkowników będzie zapamiętanie i sprawdzenie, czy podobiekt tego typu powinien zostać skopiowany za pomocą konstruktora quasi-kopiującego.


Nie kopiuj stanu, który jest silnie powiązany z tożsamością obiektu

W rzadkich przypadkach podzbiór obserwowalnego stanu obiektu może stanowić (lub być uważany) za nieodłączną część tożsamości obiektu i nie powinien być przenoszony na inne obiekty (chociaż może to być nieco kontrowersyjne).

Przykłady:

  • UID obiektu (ale ten również należy do przypadku "samorejestracji" z góry, ponieważ identyfikator należy uzyskać w akcie samorejestracji).

  • Historia obiektu (np. Stos Cofnij / Ponów) w przypadku, gdy nowy obiekt nie może dziedziczyć historii obiektu źródłowego, ale zamiast tego rozpoczynać się od pojedynczego elementu historii „ Skopiowano o <CZAS> z <OTHER_OBJECT_ID> ”.

W takich przypadkach konstruktor kopiujący musi pominąć kopiowanie odpowiednich podobiektów.


Wymuszanie poprawnego podpisu konstruktora kopiującego

Podpis konstruktora kopiującego dostarczonego przez kompilator zależy od tego, jakie konstruktory kopiujące są dostępne dla podobiektów. Jeśli co najmniej jeden podobiekt nie ma prawdziwego konstruktora kopiującego (pobierającego obiekt źródłowy przez stałe odniesienie), ale zamiast tego ma mutującego konstruktora kopiującego (pobierającego obiekt źródłowy przez niestałe odniesienie), kompilator nie będzie miał wyboru ale niejawnie zadeklarować, a następnie zdefiniować mutujący konstruktor kopiujący.

A co, jeśli „mutujący” konstruktor kopiujący typu podobiektu w rzeczywistości nie mutuje obiektu źródłowego (i został po prostu napisany przez programistę, który nie wie o constsłowie kluczowym)? Jeśli nie możemy naprawić tego kodu przez dodanie brakującego const, to inną opcją jest zadeklarowanie własnego konstruktora kopiującego zdefiniowanego przez użytkownika z poprawnym podpisem i popełnienie grzechu przejścia na plik const_cast.


Kopiowanie przy zapisie (COW)

Kontener COW, który podaje bezpośrednie odniesienia do swoich danych wewnętrznych, MUSI zostać głęboko skopiowany w czasie konstruowania, w przeciwnym razie może zachowywać się jak uchwyt zliczający odniesienia.

Chociaż COW jest techniką optymalizacji, ta logika w konstruktorze kopiującym jest kluczowa dla jej poprawnej implementacji. Dlatego umieściłem ten przypadek tutaj, a nie w sekcji „Optymalizacja”, do której przechodzimy dalej.



Optymalizacja

W następujących przypadkach możesz chcieć / chcieć zdefiniować własny konstruktor kopiujący z powodów związanych z optymalizacją:


Optymalizacja struktury podczas kopiowania

Rozważmy kontener obsługujący operacje usuwania elementów, ale można to zrobić, po prostu oznaczając usunięty element jako usunięty, a następnie ponownie wykorzystaj jego gniazdo. Kiedy tworzona jest kopia takiego kontenera, sensowne może być skompaktowanie zachowanych danych zamiast zachowania „usuniętych” gniazd w takiej postaci, w jakiej są.


Pomiń kopiowanie stanu nieobserwowalnego

Obiekt może zawierać dane, które nie są częścią jego obserwowalnego stanu. Zwykle są to buforowane / zapamiętywane dane gromadzone przez cały okres istnienia obiektu w celu przyspieszenia niektórych powolnych operacji zapytań wykonywanych przez obiekt. Można bezpiecznie pominąć kopiowanie tych danych, ponieważ zostaną one ponownie obliczone, gdy (i jeśli!) Zostaną wykonane odpowiednie operacje. Kopiowanie tych danych może być nieuzasadnione, ponieważ może zostać szybko unieważnione, jeśli obserwowalny stan obiektu (z którego pochodzą dane z pamięci podręcznej) zostanie zmodyfikowany przez operacje mutacyjne (a jeśli nie zamierzamy modyfikować obiektu, to dlaczego tworzymy głęboki skopiować?)

Optymalizacja ta jest uzasadniona tylko wtedy, gdy dane pomocnicze są duże w porównaniu z danymi reprezentującymi obserwowalny stan.


Wyłącz niejawne kopiowanie

C ++ umożliwia wyłączenie niejawnego kopiowania poprzez zadeklarowanie konstruktora kopiującego explicit. Wówczas obiekty tej klasy nie mogą być przekazywane do funkcji i / lub zwracane z funkcji przez wartość. Ta sztuczka może być użyta dla typu, który wydaje się lekki, ale w rzeczywistości jest bardzo drogi do skopiowania (chociaż uczynienie go quasi-kopiowalnym może być lepszym wyborem).

W C ++ 03 zadeklarowanie konstruktora kopiującego również wymagało zdefiniowania go (oczywiście, jeśli zamierzałeś go używać). W związku z tym wybranie takiego konstruktora kopiującego tylko z omawianego problemu oznaczało, że trzeba było napisać ten sam kod, który kompilator wygeneruje automatycznie.

C ++ 11 i nowsze standardy pozwalają na deklarowanie specjalnych funkcji składowych (konstruktorów domyślnych i kopiujących, operatora przypisania kopii i destruktora) z jawnym żądaniem użycia domyślnej implementacji (po prostu zakończ deklarację =default).



TODOs

Tę odpowiedź można poprawić w następujący sposób:

  • Dodaj więcej przykładowego kodu
  • Zilustruj przypadek „Obiekty z wewnętrznymi odsyłaczami”
  • Dodaj linki

6

Jeśli masz klasę, która ma dynamicznie przydzielaną zawartość. Na przykład przechowujesz tytuł książki jako znak * i ustawiasz tytuł na nowy, kopiowanie nie będzie działać.

Musiałbyś napisać konstruktora kopiującego, który to robi, title = new char[length+1]a potem strcpy(title, titleIn). Konstruktor kopiujący wykonałby po prostu „płytką” kopię.


2

Copy Constructor jest wywoływana, gdy obiekt jest przekazywany przez wartość, zwracany przez wartość lub jawnie kopiowany. Jeśli nie ma konstruktora kopiującego, c ++ tworzy domyślny konstruktor kopiujący, który tworzy płytką kopię. Jeśli obiekt nie ma wskaźników do dynamicznie przydzielanej pamięci, zrobi to płytka kopia.


0

Często dobrym pomysłem jest wyłączenie ctor kopiowania i operator =, chyba że klasa wyraźnie tego potrzebuje. Może to zapobiec nieefektywności, takim jak przekazywanie argumentu przez wartość, gdy jest zamierzone odniesienie. Również metody wygenerowane przez kompilator mogą być nieprawidłowe.


-1

Rozważmy poniższy fragment kodu:

class base{
    int a, *p;
public:
    base(){
        p = new int;
    }
    void SetData(int, int);
    void ShowData();
    base(const base& old_ref){
        //No coding present.
    }
};
void base :: ShowData(){
    cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
    this->a = a;
    *(this->p) = b;
}
int main(void)
{
    base b1;
    b1.SetData(2, 3);
    b1.ShowData();
    base b2 = b1; //!! Copy constructor called.
    b2.ShowData();
    return 0;
}

Output: 
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();

b2.ShowData();daje niepotrzebne dane wyjściowe, ponieważ istnieje konstruktor kopiujący zdefiniowany przez użytkownika, który nie został napisany w celu jawnego kopiowania danych. Więc kompilator nie tworzy tego samego.

Pomyślałem o podzieleniu się tą wiedzą z wami wszystkimi, chociaż większość z was już to wie.

Pozdrawiam ... Miłego kodowania !!!

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.