Abstrakcyjne klasy podstawowe i konstrukcja kopii, zasady praktyczne

10

Często dobrym pomysłem jest posiadanie abstrakcyjnej klasy bazowej do izolowania interfejsu obiektu.

Problem polega na tym, że konstrukcja kopiowania, IMHO, jest domyślnie dość zepsuta w C ++, przy czym domyślnie generowane są konstruktory kopiowania.

Więc jakie są problemy, kiedy masz abstrakcyjną klasę bazową i surowe wskaźniki w klasach pochodnych?

class IAbstract
{
    ~IAbstract() = 0;
}

class Derived : public IAbstract
{
    char *theProblem;
    ...
}

IAbstract *a1 = new Derived();
IAbstract a2 = *a1;//???

A teraz czy całkowicie wyłączasz tworzenie kopii dla całej hierarchii? Czy deklarujesz budowę kopii jako prywatną w IAbstract?

Czy są jakieś trzy reguły z abstrakcyjnymi klasami podstawowymi?

Koder
źródło
1
używaj referencji zamiast wskaźników :)
tp1
@ tp1: lub przynajmniej jakiś inteligentny wskaźnik.
Benjamin Bannier,
1
Czasami musisz po prostu pracować z istniejącym kodem ... Nie możesz zmienić wszystkiego w jednej chwili.
Koder
Jak myślisz, dlaczego domyślny konstruktor kopii jest uszkodzony?
BЈовић
2
@Coder: Przewodnik po stylu Google to kupa śmieci i absolutnie do bani dla każdego projektu w C ++.
DeadMG

Odpowiedzi:

6

Kopiowanie konstrukcji klasy abstrakcyjnej powinno być w większości przypadków prywatne, a także operatora przypisania.

Klasy abstrakcyjne są, z definicji, typem polimorficznym. Nie wiesz więc, ile pamięci używa Twoja instancja, więc nie możesz jej bezpiecznie skopiować ani przypisać. W praktyce ryzykujesz krojenie: /programming/274626/what-is-the-slicing-problem-in-c

Typu polimorficznego w C ++ nie można manipulować wartością. Manipulujesz nimi za pomocą odwołania lub wskaźnika (lub dowolnego inteligentnego wskaźnika).

To jest powód, dla którego Java sprawiła, że ​​obiektami można manipulować tylko przez odniesienie, i dlaczego C # i D mają separację między klasami i strukturami (pierwsza z nich jest polimorficzna i typem referencyjnym, druga nie jest polimorficzna i typem wartości).

deadalnix
źródło
2
Rzeczy w Javie nie są lepsze. W Javie kopiowanie czegokolwiek jest bolesne, nawet jeśli naprawdę tego potrzebujesz, i bardzo łatwo o tym zapomnieć. W efekcie powstają nieprzyjemne błędy, w których dwie struktury danych mają odniesienie do tej samej klasy wartości (np. Data), a następnie jedna z nich zmienia datę, a druga struktura danych jest teraz zepsuta.
kevin cline
3
Meh To żaden problem - każdy, kto zna C ++, nie popełni tego błędu. Co ważniejsze, nie ma żadnego powodu, aby manipulować typami polimorficznymi pod względem wartości, jeśli wiesz, co robisz. Inicjujesz całkowitą reakcję szarpnięcia kolanem na technicznie wąską możliwość. Wskaźniki NULL są większym problemem.
DeadMG
8

Oczywiście możesz po prostu uczynić go chronionym i pustym, aby klasy pochodne mogły wybierać. Jednak bardziej ogólnie, twój kod jest i tak zabroniony, ponieważ nie można go utworzyć IAbstract- ponieważ ma czystą funkcję wirtualną. W związku z tym nie stanowi to problemu - twoich klas interfejsów nie można utworzyć i dlatego nie można ich skopiować, a bardziej pochodne klasy mogą blokować lub kontynuować kopiowanie, jak chcą.

DeadMG
źródło
A co, jeśli istnieje hierarchia Abstrakt -> Derived1 -> Derived2, a Derived2 jest przypisany do Derived1? Te mroczne zakątki języka wciąż mnie zaskakują.
Koder
@ Koder: Zwykle jest to postrzegane jako PEBKAC. Jednak bardziej bezpośrednio, zawsze można zadeklarować prywatną operator=(const Derived2&)in Derived1.
DeadMG
Ok, może to naprawdę nie jest problem. Chcę tylko upewnić się, że klasa jest bezpieczna przed nadużyciami.
Koder
2
I za to powinieneś dostać medal.
Inżynier świata,
2
@Coder: Nadużycie przez kogo? Najlepsze, co możesz zrobić, to ułatwić pisanie poprawnego kodu. Nie ma sensu próbować bronić się przed programistami, którzy nie znają C ++.
kevin cline
2

Ustawiając ctor i przypisanie na prywatne (lub deklarując je jako = usuń w C ++ 11) wyłączasz kopiowanie.

Chodzi o to, GDZIE musisz to zrobić. Aby zachować kod, IAbstract nie stanowi problemu. (zwróć uwagę, że robiąc to, co zrobiłeś, przypisujesz *a1 IAbstractpodobiekt do a2, tracąc wszelkie odniesienia do Derived. Przypisanie wartości nie jest polimorficzne)

Problem pochodzi z Derived::theproblem. Skopiowanie pochodnej do innej może w rzeczywistości współdzielić *theproblemdane, które mogą nie być przeznaczone do współdzielenia (istnieją dwa przypadki, które mogą wywoływać delete theproblemich destruktor).

W takim przypadku Derivedmusi to być niemożliwe do skopiowania i nieprzenoszalne. Oczywiście, jeśli ustawisz kopię jako prywatną IAbstract, ponieważ domyślna kopia tego Derivedpotrzebuje, Derivedrównież nie będzie można jej skopiować . Ale jeśli zdefiniujesz własne Derived::Derived(const Derived&)bez wywoływania IAbtractkopiowania, nadal możesz je skopiować.

Problem leży w pochodnej, a rozwiązanie musi pozostać w pochodnej: jeśli musi to być obiekt tylko dynamiczny, do którego dostęp mają tylko wskaźniki lub referencje, to sama pochodna musi mieć

class Derived
{
    ...
    Derived(const Derived&) = delete;
    Derived& operator=(const Derived&) = delete;
};

Zasadniczo to od projektanta klasy Derived (która powinna wiedzieć, jak działa Derived i jak theproblemzarządzać) decyduje, co zrobić z przypisaniem i kopiowaniem.

Emilio Garavaglia
źródło