Konstruktor kopiujący i przeciążenie operatora = w C ++: czy możliwa jest wspólna funkcja?


87

Ponieważ konstruktor kopiujący

MyClass(const MyClass&);

i przeciążenie operatora =

MyClass& operator = (const MyClass&);

mają prawie taki sam kod, ten sam parametr i różnią się tylko zwrotem, czy jest możliwe, aby obie miały wspólną funkcję?


6
„… mają prawie taki sam kod…”? Hmm ... Musisz robić coś złego. Spróbuj zminimalizować potrzebę używania do tego funkcji zdefiniowanych przez użytkownika i pozwól kompilatorowi wykonać całą brudną robotę. Często oznacza to hermetyzację zasobów w ich własnym obiekcie członkowskim. Możesz nam pokazać kod. Może mamy dobre sugestie dotyczące projektu.
sellibitze

Odpowiedzi:


121

Tak. Istnieją dwie typowe opcje. Jednym - co jest ogólnie odradzane - jest operator=jawne wywołanie z konstruktora kopiującego:

MyClass(const MyClass& other)
{
    operator=(other);
}

Jednak dostarczenie dobra operator=jest wyzwaniem, jeśli chodzi o radzenie sobie ze starym stanem i problemami wynikającymi z samozadania. Ponadto wszyscy członkowie i bazy są domyślnie inicjalizowane jako pierwsze, nawet jeśli mają być przypisane z other. Może to nie dotyczyć nawet wszystkich członków i baz, a nawet jeśli jest poprawne, jest semantycznie nadmiarowe i może być praktycznie kosztowne.

Coraz popularniejszym rozwiązaniem jest implementacja z operator=wykorzystaniem konstruktora kopiującego i metody swap.

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

lub nawet:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

swapFunkcja jest zwykle prosty do napisania, jak to tylko zamienia własność wewnętrznych i nie trzeba oczyścić istniejącego stanu lub przeznaczyć nowe zasoby.

Zaletą idiomu kopiowania i zamiany jest to, że jest on automatycznie bezpieczny przy samoczynnym przypisywaniu i - pod warunkiem, że operacja zamiany nie jest rzutowana - jest również silnie bezpieczna dla wyjątków.

Aby być silnie zabezpieczonym przed wyjątkami, operator przypisania „odręcznego” zazwyczaj musi przydzielić kopię nowych zasobów przed cofnięciem alokacji starych zasobów cesjonariusza, tak aby w przypadku wystąpienia wyjątku przy przydzielaniu nowych zasobów stary stan nadal mógł zostać przywrócony do . Wszystko to jest dostępne za darmo z funkcją kopiowania i wymiany, ale zwykle jest bardziej złożone, a zatem podatne na błędy, do zrobienia od zera.

Jedyną rzeczą, na którą należy uważać, jest upewnienie się, że metoda swap jest prawdziwą zamianą, a nie domyślną, std::swapktóra używa samego konstruktora kopiującego i operatora przypisania.

Zwykle używany swapjest element członkowski . std::swapdziała i jest gwarantowany bez rzutowania w przypadku wszystkich podstawowych typów i typów wskaźników. Większość inteligentnych wskaźników można również zamienić z gwarancją braku rzucania.


3
W rzeczywistości nie są to zwykłe operacje. Podczas gdy kontroler kopiujący po raz pierwszy inicjuje elementy członkowskie obiektu, operator przypisania zastępuje istniejące wartości. Biorąc to pod uwagę, operator=tworzenie allingów z kontrolera kopiującego jest w rzeczywistości dość złe, ponieważ najpierw inicjalizuje wszystkie wartości do pewnych wartości domyślnych, aby zaraz potem zastąpić je wartościami innego obiektu.
sbi

14
Może do „Nie polecam”, dodaj „i żaden ekspert C ++ też nie”. Ktoś może przyjść i nie zdać sobie sprawy, że nie wyrażasz tylko osobistych preferencji mniejszości, ale ustaloną, zgodną opinię tych, którzy faktycznie o tym pomyśleli. I OK, może się mylę i jakiś ekspert od C ++ poleca to, ale osobiście nadal postawiłbym rękawicę komuś, kto wymyśli odniesienie do tej rekomendacji.
Steve Jessop

4
W porządku, i tak cię już zagłosowałem :-). Doszedłem do wniosku, że jeśli coś jest powszechnie uważane za najlepszą praktykę, to najlepiej jest to powiedzieć (i spójrz na to jeszcze raz, jeśli ktoś mówi, że to wcale nie jest najlepsze). Podobnie, gdyby ktoś zapytał „czy można używać muteksów w C ++”, nie powiedziałbym, że „jedną dość powszechną opcją jest całkowite zignorowanie RAII i napisanie kodu niezabezpieczonego przed wyjątkami, który blokuje się w środowisku produkcyjnym, ale coraz popularniejsze jest pisanie porządny, działający kod ";-)
Steve Jessop

4
+1. Myślę, że zawsze potrzebna jest analiza. Myślę, że rozsądne jest, aby assignw niektórych przypadkach (dla lekkich klas) funkcja składowa była używana zarówno przez kontrolera kopiującego, jak i operatora przypisania. W innych przypadkach (przypadki wymagające dużej ilości zasobów / przypadków używania, uchwyt / treść) kopiowanie / zamiana jest oczywiście drogą.
Johannes Schaub - litb

2
@litb: Zaskoczyło mnie to, więc odszukałem pozycję 41 w Exception C ++ (w który to się zmieniło) i ta konkretna rekomendacja zniknęła, a on zaleca kopiowanie i zamianę w jego miejsce. Raczej podstępnie rzucił „Problem # 4: To nieefektywne do przydziału” w tym samym czasie.
CB Bailey,

13

Konstruktor kopiujący wykonuje pierwszą inicjalizację obiektów, które wcześniej były pamięcią surową. Operator przypisania OTOH zastępuje istniejące wartości nowymi. Częściej niż nigdy wiąże się to z odrzuceniem starych zasobów (na przykład pamięci) i przydzieleniem nowych.

Jeśli istnieje podobieństwo między nimi, oznacza to, że operator przypisania wykonuje niszczenie i tworzenie kopii. Niektórzy programiści faktycznie implementowali przypisywanie poprzez niszczenie w miejscu, po którym następowało tworzenie kopii zapasowej. Jest to jednak bardzo zły pomysł. (A co, jeśli jest to operator przypisania klasy bazowej, który wywołał podczas przypisywania klasy pochodnej?)

Obecnie używa się tego, co zwykle uważa się za idiom kanoniczny swap zgodnie z sugestią Charlesa:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

Używa konstrukcji kopiowania (uwaga, która otherjest kopiowana) i niszczenia (jest niszczona na końcu funkcji) - i używa ich również we właściwej kolejności: konstrukcja (może zawieść) przed zniszczeniem (nie może zawieść).


Należy swapzadeklarować virtual?

1
@Johannes: Funkcje wirtualne są używane w polimorficznych hierarchiach klas. Operatory przypisania są używane dla typów wartości. Te dwa prawie się nie mieszają.
sbi

-3

Coś mnie niepokoi:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

Po pierwsze, czytanie słowa „zamień”, kiedy myślę „kopiuj”, irytuje mój zdrowy rozsądek. Kwestionuję też cel tej wymyślnej sztuczki. Tak, wszelkie wyjątki przy konstruowaniu nowych (kopiowanych) zasobów powinny mieć miejsce przed wymianą, co wydaje się bezpiecznym sposobem na upewnienie się, że wszystkie nowe dane zostały wypełnione przed ich uruchomieniem.

W porządku. A co z wyjątkami, które mają miejsce po zamianie? (gdy stare zasoby zostaną zniszczone, gdy tymczasowy obiekt wyjdzie poza zakres) Z punktu widzenia użytkownika przypisania operacja nie powiodła się, chyba że tak się nie stało. Ma to ogromny efekt uboczny: kopia faktycznie się wydarzyła. Nie udało się tylko wyczyścić zasoby. Stan obiektu docelowego został zmieniony, mimo że operacja wydaje się z zewnątrz nie powiodła się.

Dlatego proponuję zamiast „zamiany” zrobić bardziej naturalny „transfer”:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    transfer(tmp);
    return *this;
}

Wciąż trwa konstrukcja tymczasowego obiektu, ale następną natychmiastową czynnością jest uwolnienie wszystkich bieżących zasobów miejsca docelowego przed przeniesieniem (i ZEROWANIE, aby nie zostały one podwójnie zwolnione) zasobów źródła do niego.

Zamiast {construct, move, destruct}, proponuję {construct, destruct, move}. Ruch, który jest najniebezpieczniejszą akcją, jest wykonywany jako ostatni, gdy wszystko inne zostanie ustalone.

Tak, niepowodzenie zniszczenia jest problemem w obu schematach. Dane są uszkodzone (skopiowane, jeśli nie sądziłeś, że są) lub utracone (uwolnione, gdy nie sądziłeś, że tak jest). Zgubiony jest lepszy niż zepsuty. Żadne dane nie są lepsze niż złe dane.

Transfer zamiast zamiany. W każdym razie to moja sugestia.


2
Destruktor nie może zawieść, więc nie oczekuje się wyjątków po zniszczeniu. I nie rozumiem, jaka byłaby korzyść z przesunięcia ruchu za zniszczeniem, jeśli ruch jest najbardziej niebezpieczną operacją? Oznacza to, że w standardowym schemacie niepowodzenie przenoszenia nie uszkodzi starego stanu, podczas gdy nowy schemat tak. Więc dlaczego? Ponadto First, reading the word "swap" when my mind is thinking "copy" irritates-> Jako pisarz w bibliotece zazwyczaj znasz powszechne praktyki (kopiowanie + zamiana), a sednem jest my mind. Twój umysł jest faktycznie ukryty za publicznym interfejsem. Na tym polega kod wielokrotnego użytku.
Sebastian Mach
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.