Zasada 5 - z niej korzystać czy nie?


20

Zasada 3 ( zasada 5 w nowym standardzie c ++) stanowi:

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

Ale z drugiej strony „ Czysty kod ” Martina zaleca usunięcie wszystkich pustych konstruktorów i destruktorów (strona 293, G12: Clutter ):

Jakie zastosowanie ma domyślny konstruktor bez implementacji? Wystarczy zaśmiecać kod bezsensownymi artefaktami.

Jak więc poradzić sobie z tymi dwiema przeciwnymi opiniami? Czy naprawdę należy wdrożyć puste konstruktory / destruktory?


Następny przykład pokazuje dokładnie, co mam na myśli:

#include <iostream>
#include <memory>

struct A
{
    A( const int value ) : v( new int( value ) ) {}
    ~A(){}
    A( const A & other ) : v( new int( *other.v ) ) {}
    A& operator=( const A & other )
    {
        v.reset( new int( *other.v ) );
        return *this;
    }

    std::auto_ptr< int > v;
};
int main()
{
    const A a( 55 );
    std::cout<< "a value = " << *a.v << std::endl;
    A b(a);
    std::cout<< "b value = " << *b.v << std::endl;
    const A c(11);
    std::cout<< "c value = " << *c.v << std::endl;
    b = c;
    std::cout<< "b new value = " << *b.v << std::endl;
}

Kompiluje się dobrze przy użyciu g ++ 4.6.1 z:

g++ -std=c++0x -Wall -Wextra -pedantic example.cpp

Destruktor struct Ajest pusty i nie jest tak naprawdę potrzebny. Czy powinien tam być, czy powinien zostać usunięty?


15
Te 2 cytaty mówią o różnych rzeczach. Albo zupełnie mi brakuje twojej racji.
Benjamin Bannier

1
@honk W standardzie kodowania mojego zespołu mamy zasadę, aby zawsze deklarować wszystkie 4 (konstruktor, destruktor, konstruktor kopiujący). Zastanawiałem się, czy to naprawdę ma sens. Czy naprawdę muszę zawsze deklarować destruktory, nawet jeśli są puste?
BЈовић

Jeśli chodzi o puste desctructors, pomyśl o tym: codesynthesis.com/~boris/blog/2012/04/04/… . W przeciwnym razie zasada 3 (5) ma dla mnie idealny sens, nie mam pojęcia, dlaczego ktoś chciałby reguły 4
Benjamin Bannier

@honk Uważaj na informacje, które znajdziesz w sieci. Nie wszystko jest prawdą. Na przykład virtual ~base () = default;nie kompiluje się (bez uzasadnionego powodu)
BЈовић

@VJovic, Nie, nie musisz deklarować pustego destruktora, chyba że musisz go uczynić wirtualnym. A skoro jesteśmy na ten temat, nie powinieneś również używać auto_ptr.
Dima,

Odpowiedzi:


44

Na początek reguła mówi „prawdopodobnie”, więc nie zawsze ma zastosowanie.

Drugą kwestią, którą tu widzę, jest to, że jeśli musisz zadeklarować jedną z trzech, dzieje się tak, ponieważ robi coś specjalnego, na przykład przydział pamięci. W tym przypadku inne nie byłyby puste, ponieważ musiałyby poradzić sobie z tym samym zadaniem (takim jak kopiowanie zawartości dynamicznie alokowanej pamięci w konstruktorze kopiowania lub zwolnienie takiej pamięci).

Podsumowując, nie powinieneś deklarować pustych konstruktorów ani destruktorów, ale bardzo prawdopodobne jest, że jeśli jeden jest potrzebny, inne też są potrzebne.

Na przykład: W takim przypadku możesz pominąć destruktor. Oczywiście nic nie robi. Zastosowanie inteligentnych wskaźników jest doskonałym przykładem tego, gdzie i dlaczego zasada 3 nie ma zastosowania.

Jest to tylko wskazówka, gdzie możesz ponownie przyjrzeć się kodowi na wypadek, gdybyś zapomniał wdrożyć ważną funkcjonalność, której w przeciwnym razie mógłbyś nie zauważyć.


Dzięki zastosowaniu inteligentnych wskaźników, destruktory są w większości przypadków puste (powiedziałbym, że> 99% destruktorów w mojej bazie kodu jest puste, ponieważ prawie każda klasa używa idiomu pimpl).
BЈовић

Wow, to tyle krosty, że nazwałbym to śmierdzącym. W wielu kompilatorach pimpled będzie trudniejszy do optymalizacji (np. Trudniejszy do wbudowania).
Benjamin Bannier

@honk Co rozumiesz przez „wiele kompilatorów wypróbowanych”? :)
BЈовић

@VJovic: przepraszam, literówka: „pimpled code”
Benjamin Bannier

4

Naprawdę nie ma tutaj sprzeczności. Reguła 3 mówi o destruktorze, konstruktorze kopii i operatorze przypisania kopii. Wujek Bob mówi o pustych domyślnych konstruktorach.

Jeśli potrzebujesz destruktora, Twoja klasa prawdopodobnie zawiera wskaźniki do dynamicznie alokowanej pamięci i prawdopodobnie chcesz mieć ctor kopiowania i operator=()kopię głęboką. Jest to całkowicie ortogonalne w stosunku do tego, czy potrzebujesz domyślnego konstruktora.

Zauważ też, że w C ++ są sytuacje, gdy potrzebujesz domyślnego konstruktora, nawet jeśli jest on pusty. Powiedzmy, że twoja klasa ma domyślnego konstruktora. W takim przypadku kompilator nie wygeneruje domyślnego konstruktora. Oznacza to, że obiekty tej klasy nie mogą być przechowywane w kontenerach STL, ponieważ te kontenery oczekują, że obiekty będą mogły zostać zbudowane domyślnie.

Z drugiej strony, jeśli nie planujesz nigdy umieszczać obiektów swojej klasy w kontenerach STL, pusty domyślny konstruktor z pewnością jest niepotrzebnym bałaganem.


2

Tutaj twój potencjalny (*) ekwiwalent domyślnego jednego konstruktora / przypisania / destruktora ma cel: udokumentuj swoje przemyślenia na temat problemu i ustal, że domyślne zachowanie jest prawidłowe. BTW, w C ++ 11, rzeczy nie ustabilizowały się wystarczająco, aby wiedzieć, czy =defaultmogą służyć temu celowi.

(Jest jeszcze inny potencjalny cel: podaj definicję poza linią zamiast domyślnej definicji wbudowanej, lepiej udokumentuj jawnie, jeśli masz ku temu powód).

(*) Potencjał, ponieważ nie pamiętam prawdziwego przypadku, w którym zasada trzech nie miała zastosowania, gdybym musiał coś zrobić w jednym, musiałbym zrobić coś w innych.


Edytuj po dodaniu przykładu. twój przykład użycia auto_ptr jest interesujący. Używasz inteligentnego wskaźnika, ale nie takiego, który jest odpowiedni do zadania. Wolę napisać taki, który jest - zwłaszcza jeśli sytuacja często się zdarza - niż robić to, co zrobiłeś. (Jeśli się nie mylę, ani standard, ani boost nie zapewniają).


Przykład pokazuje mój punkt widzenia. Destruktor nie jest tak naprawdę potrzebny, ale zasada 3 mówi, że powinien tam być.
BЈовић

1

Reguła 5 jest kautalatywnym rozszerzeniem reguły 3, która jest kautelatywnym zachowaniem przed możliwym niewłaściwym użyciem obiektu.

Jeśli potrzebujesz destruktora, oznacza to, że wykonałeś „zarządzanie zasobami” inne niż domyślne (po prostu konstruuj i niszcz wartości ).

Ponieważ kopiowanie, przypisywanie, przenoszenie i przenoszenie domyślnych wartości kopiowania jest niemożliwe, jeśli nie przechowujesz tylko wartości , musisz określić, co należy zrobić.

To powiedziawszy, C ++ usuwa kopię, jeśli zdefiniujesz ruch i usuwa ruch, jeśli zdefiniujesz kopię. W większości przypadków musisz zdefiniować, czy chcesz emulować wartość (stąd kopiowanie mut klonuje zasób, a ruch nie ma sensu) lub menedżer zasobów (a zatem przenosi zasób, w którym kopia nie ma sensu: reguła 3 staje się regułą pozostałych 3 )

Przypadki, w których musisz zdefiniować zarówno kopiowanie, jak i przenoszenie (reguła 5) są dość rzadkie: zazwyczaj masz „dużą wartość”, którą należy skopiować, jeśli podano ją odrębnym obiektom, ale można ją przenieść, jeśli zostanie pobrana z obiektu tymczasowego (unikając klon następnie zniszczyć ). Tak jest w przypadku pojemników STL lub pojemników arytmetycznych.

Sprawą mogą być macierze: muszą obsługiwać kopiowanie, ponieważ wartościami ( a=b; c=b; a*=2; b*=3;nie mogą wpływać na siebie nawzajem), ale można je optymalizować, wspierając także przenoszenie ( a = 3*b+4*cma taki, +który zajmuje dwa tymczasowe i generuje tymczasowe: unikanie klonowania i usuwania może być przydatny)


1

Wolę inne sformułowanie reguły trzech, co wydaje się bardziej rozsądne, a mianowicie „jeśli twoja klasa potrzebuje destruktora (innego niż pusty wirtualny destruktor), prawdopodobnie również potrzebuje konstruktora kopii i operatora przypisania”.

Określenie go jako relacji jednokierunkowej z destruktora wyjaśnia kilka rzeczy:

  1. Nie dotyczy to przypadków, w których podajesz niestandardowego konstruktora kopii lub operatora przypisania wyłącznie jako optymalizację.

  2. Powodem tej reguły jest to, że domyślny konstruktor kopii lub operator przypisania może zepsuć ręczne zarządzanie zasobami. Jeśli ręcznie zarządzasz zasobami, prawdopodobnie zdajesz sobie sprawę, że potrzebujesz niszczyciela, aby je uwolnić.


-3

Jest jeszcze jedna kwestia, o której jeszcze nie wspomniano w dyskusji: Destruktor powinien zawsze być wirtualny.

struct A
{
    A( const int value ) : v( new int( value ) ) {}
    virtual ~A(){}
    ...
}

Konstruktor musi zostać zadeklarowany jako wirtualny w klasie bazowej, aby uczynić go wirtualnym również we wszystkich klasach pochodnych. Tak więc, nawet jeśli twoja klasa podstawowa nie potrzebuje destruktora, ostatecznie deklarujesz i implementujesz pusty destruktor.

Jeśli włączysz wszystkie ostrzeżenia na (-Wall -Wextra -Weffc ++), g ++ cię o tym ostrzeże. Uważam, że dobrą praktyką jest zawsze deklarowanie wirtualnego destruktora w dowolnej klasie, ponieważ nigdy nie wiadomo, czy klasa ostatecznie stanie się klasą podstawową. Jeśli wirtualny destruktor nie jest potrzebny, nie zaszkodzi. Jeśli tak, oszczędzasz czas na znalezienie błędu.


1
Ale nie chcę wirtualnego konstruktora. Jeśli to zrobię, każde wywołanie dowolnej metody będzie korzystało z wirtualnej wysyłki. przy okazji zwróć uwagę, że w c ++ nie ma czegoś takiego jak „wirtualny konstruktor”. Ponadto skompilowałem przykład jako bardzo wysoki poziom ostrzeżenia.
BЈовић

IIRC, reguła używana przez gcc jako ostrzeżenie i reguła, którą i tak ogólnie przestrzegam, mówi, że powinien istnieć wirtualny destruktor, jeśli istnieją inne wirtualne metody w klasie.
Jules
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.