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 A
jest pusty i nie jest tak naprawdę potrzebny. Czy powinien tam być, czy powinien zostać usunięty?
źródło
virtual ~base () = default;
nie kompiluje się (bez uzasadnionego powodu)auto_ptr
.Odpowiedzi:
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ć.
źródło
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.
źródło
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
=default
mogą 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ą).
źródło
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ż są 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*c
ma taki,+
który zajmuje dwa tymczasowe i generuje tymczasowe: unikanie klonowania i usuwania może być przydatny)źródło
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:
Nie dotyczy to przypadków, w których podajesz niestandardowego konstruktora kopii lub operatora przypisania wyłącznie jako optymalizację.
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ć.
źródło
Jest jeszcze jedna kwestia, o której jeszcze nie wspomniano w dyskusji: Destruktor powinien zawsze być wirtualny.
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.
źródło