Myślałem: mówią, że jeśli dzwonisz do destruktora ręcznie - robisz coś nie tak. Ale czy zawsze tak jest? Czy są jakieś kontrprzykłady? Sytuacje, w których konieczne jest ręczne wywołanie lub w których uniknięcie tego jest trudne / niemożliwe / niepraktyczne?
c++
coding-style
destructor
Fioletowa żyrafa
źródło
źródło
new
celu zainicjowania nowego obiektu w miejsce starego. Generalnie nie jest to dobry pomysł, ale nie jest to niespotykane.Odpowiedzi:
Ręczne wywołanie destruktora jest wymagane, jeśli obiekt został skonstruowany przy użyciu przeciążonej postaci
operator new()
, z wyjątkiem sytuacji, gdy używanostd::nothrow
przeciążeń " ":T* t0 = new(std::nothrow) T(); delete t0; // OK: std::nothrow overload void* buffer = malloc(sizeof(T)); T* t1 = new(buffer) T(); t1->~T(); // required: delete t1 would be wrong free(buffer);
Zewnętrzne zarządzanie pamięcią na raczej niskim poziomie, jak powyżej jawne wywoływanie destruktorów, jest jednak oznaką złego projektu. Prawdopodobnie nie jest to tylko zły projekt, ale wręcz błędny (tak, użycie jawnego destruktora, po którym następuje wywołanie konstruktora kopiującego w operatorze przypisania, jest złym projektem i prawdopodobnie będzie błędne).
W C ++ 2011 istnieje jeszcze jeden powód, dla którego warto używać jawnych wywołań destruktorów: podczas używania uogólnionych związków konieczne jest jawne zniszczenie bieżącego obiektu i utworzenie nowego obiektu przy użyciu umieszczania new podczas zmiany typu reprezentowanego obiektu. Ponadto po zniszczeniu unii konieczne jest jawne wywołanie destruktora bieżącego obiektu, jeśli wymaga on zniszczenia.
źródło
operator new
”, poprawnym wyrażeniem jest „używanieplacement new
”.operator new(std::size_t, void*)
(i wariacji tablicy), ale raczej o wszystkich przeciążonych wersjachoperator new()
.temp = Class(object); temp.operation(); object.~Class(); object = Class(temp); temp.~Class();
yes, using an explicit destructor followed by a copy constructor call in the assignment operator is a bad design and likely to be wrong
. Dlaczego to powiedziałeś? Myślę, że jeśli destruktor jest trywialny lub prawie trywialny, to ma minimalne obciążenie i zwiększa wykorzystanie zasady DRY. Jeśli zostanie użyty w takich przypadkach z ruchemoperator=()
, może być nawet lepszy niż użycie zamiany. YMMV.virtual
funkcje (virtual
funkcje nie zostaną odtworzone), a inaczej obiekt jest tylko częściowo [re-] konstruowany.Wszystkie odpowiedzi opisują konkretne przypadki, ale jest odpowiedź ogólna:
Wywołujesz dtor jawnie za każdym razem, gdy musisz po prostu zniszczyć obiekt (w sensie C ++) bez zwalniania pamięci, w której obiekt się znajduje.
Zwykle dzieje się tak we wszystkich sytuacjach, w których alokacja / zwalnianie pamięci jest zarządzana niezależnie od konstrukcji / zniszczenia obiektu. W takich przypadkach konstrukcja odbywa się poprzez umieszczenie new na istniejącym fragmencie pamięci, a zniszczenie następuje poprzez jawne wywołanie dtor.
Oto surowy przykład:
{ char buffer[sizeof(MyClass)]; { MyClass* p = new(buffer)MyClass; p->dosomething(); p->~MyClass(); } { MyClass* p = new(buffer)MyClass; p->dosomething(); p->~MyClass(); } }
Innym godnym uwagi przykładem jest wartość domyślna,
std::allocator
gdy jest używana przezstd::vector
: elementy są konstruowane wvector
trakciepush_back
, ale pamięć jest alokowana w fragmentach, więc istnieje przed utworzeniem elementu. A zatemvector::erase
musi zniszczyć elementy, ale niekoniecznie zwalnia pamięć (zwłaszcza jeśli wkrótce ma nastąpić nowy push_back ...).Jest to „zły projekt” w ścisłym sensie OOP (powinieneś zarządzać obiektami, a nie pamięcią: fakt, że obiekty wymagają pamięci jest „incydentem”), jest to „dobry projekt” w „programowaniu niskopoziomowym” lub w przypadkach, gdy pamięć jest nie pochodzi z „darmowego sklepu”, w którym domyślnie
operator new
kupuje.Jest to zły projekt, jeśli dzieje się to losowo wokół kodu, dobry projekt, jeśli dzieje się to lokalnie w klasach specjalnie zaprojektowanych do tego celu.
źródło
Nie, nie powinieneś nazywać tego wprost, ponieważ zostałby wywołany dwukrotnie. Raz dla wywołania ręcznego, a innym razem, gdy kończy się zakres, w którym zadeklarowano obiekt.
Na przykład.
Jeśli naprawdę potrzebujesz wykonać te same operacje, powinieneś mieć oddzielną metodę.
Istnieje specyficzna sytuacja, w której możesz chcieć wywołać destruktor na dynamicznie przydzielonym obiekcie z umiejscowieniem,
new
ale nie brzmi to na coś, czego kiedykolwiek będziesz potrzebować.źródło
Nie, zależy od sytuacji, czasami jest to uzasadniony i dobry projekt.
Aby zrozumieć, dlaczego i kiedy należy jawnie wywoływać destruktory, przyjrzyjmy się, co się dzieje z „nowym” i „usuń”.
Aby dynamicznie utworzyć obiekt,
T* t = new T;
pod maską: 1. przydzielany jest rozmiar pamięci (T). 2. Konstruktor T jest wywoływany w celu zainicjowania przydzielonej pamięci. Operator new robi dwie rzeczy: alokację i inicjalizację.Aby zniszczyć obiekt
delete t;
pod maską: 1. Wzywa się destruktor T. 2. pamięć przydzielona dla tego obiektu zostaje zwolniona. operator delete robi również dwie rzeczy: zniszczenie i cofnięcie alokacji.Jeden pisze konstruktor, aby przeprowadzał inicjalizację, a destruktor, aby przeprowadzał niszczenie. Kiedy jawnie wywołujesz destruktor, następuje tylko zniszczenie, ale nie cofnięcie alokacji .
Dlatego uzasadnione użycie jawnego wywołania destruktora mogłoby brzmieć: „Chcę tylko zniszczyć obiekt, ale nie (lub nie mogę) zwolnić alokacji pamięci (jeszcze)”.
Typowym tego przykładem jest wstępne przydzielanie pamięci dla puli określonych obiektów, które w innym przypadku muszą być przydzielane dynamicznie.
Tworząc nowy obiekt, otrzymujesz porcję pamięci ze wstępnie przydzielonej puli i wykonujesz „umieszczenie nowego”. Po zakończeniu pracy z obiektem możesz chcieć jawnie wywołać destruktor, aby zakończyć czyszczenie, jeśli takie ma miejsce. Ale tak naprawdę nie zwolnisz pamięci, tak jak zrobiłby to operator delete. Zamiast tego zwracasz fragment do puli w celu ponownego wykorzystania.
źródło
Jak cytowano w FAQ, powinieneś wywołać destruktor jawnie, używając umieszczania new .
Zgadzam się jednak, że rzadko jest to potrzebne.
źródło
Za każdym razem, gdy zajdzie potrzeba oddzielenia alokacji od inicjalizacji, konieczne będzie ręczne umieszczenie nowego i jawnego wywołania destruktora. Dziś rzadko jest to konieczne, ponieważ mamy standardowe kontenery, ale jeśli musisz zaimplementować jakiś nowy rodzaj kontenera, będziesz go potrzebować.
źródło
Są przypadki, kiedy są konieczne:
W kodzie, nad którym pracuję, używam jawnego wywołania destruktora w alokatorach, mam implementację prostego alokatora, który używa umieszczania new do zwracania bloków pamięci do kontenerów stl. W zniszczeniu mam:
void destroy (pointer p) { // destroy objects by calling their destructor p->~T(); }
podczas konstruowania:
void construct (pointer p, const T& value) { // initialize memory with placement new #undef new ::new((PVOID)p) T(value); }
alokacja jest również wykonywana w funkcji assign (), a zwalnianie pamięci w funkcji deallocate (), przy użyciu mechanizmów przydzielania i zwalniania specyficznych dla platformy. Ten alokator był używany do ominięcia doug lea malloc i użycia bezpośrednio, na przykład LocalAlloc w systemie Windows.
źródło
Znalazłem 3 sytuacje, w których musiałem to zrobić:
źródło
Nigdy nie spotkałem się z sytuacją, w której trzeba by ręcznie wywołać destruktor. Wydaje mi się, że nawet Stroustrup twierdzi, że to zła praktyka.
źródło
C+
☺A co z tym?
Destructor nie jest wywoływany, jeśli wyjątek jest wyrzucany z konstruktora, więc muszę wywołać go ręcznie, aby zniszczyć uchwyty, które zostały utworzone w konstruktorze przed wyjątkiem.
class MyClass { HANDLE h1,h2; public: MyClass() { // handles have to be created first h1=SomeAPIToCreateA(); h2=SomeAPIToCreateB(); try { ... if(error) { throw MyException(); } } catch(...) { this->~MyClass(); throw; } } ~MyClass() { SomeAPIToDestroyA(h1); SomeAPIToDestroyB(h2); } };
źródło
ctor
tutaj napisałeś , jest zły, dokładnie z powodu, który sam podałeś: jeśli alokacja zasobów się nie powiedzie, występuje problem z czyszczeniem. „Ktor” nie powinien wzywaćthis->~dtor()
.dtor
należy wywołać na skonstruowanych obiektach, aw tym przypadku obiekt nie jest jeszcze skonstruowany. Cokolwiek się stanie,ctor
powinno zająć się czyszczeniem. Wewnątrzctor
kodu powinieneś używać narzędzi takich jakstd::unique_ptr
do obsługi automatycznego czyszczenia w przypadkach, gdy coś się wyrzuca. ZmianaHANDLE h1, h2
pól w klasie w celu obsługi automatycznego czyszczenia może być również dobrym pomysłem.MyClass(){ cleanupGuard1<HANDLE> tmp_h1(&SomeAPIToDestroyA) = SomeAPIToCreateA(); cleanupGuard2<HANDLE> tmp_h2(&SomeAPIToDestroyB) = SomeAPIToCreateB(); if(error) { throw MyException(); } this->h1 = tmp_h1.release(); this->h2 = tmp_h2.release(); }
i to wszystko . Bez ryzykownego ręcznego czyszczenia, bez przechowywania uchwytów w częściowo zbudowanym obiekcie, dopóki wszystko nie będzie bezpieczne, jest bonusem. Jeśli zmieniszHANDLE h1,h2
klasę nacleanupGuard<HANDLE> h1;
itp., Możesz nawet w ogóle nie potrzebowaćdtor
.cleanupGuard1
icleanupGuard2
zależy od tego, coxxxToCreate
zwraca dany zwrot i jakie parametryxxxxToDestroy
przyjmuje dany parametr . Jeśli są proste, możesz nawet nie potrzebować niczego pisać, ponieważ często okazuje się, żestd::unique_ptr<x,deleter()>
(lub podobny) może załatwić sprawę w obu przypadkach.Znalazłem inny przykład, w którym musiałbyś ręcznie wywołać destruktor (y). Załóżmy, że zaimplementowałeś klasę podobną do wariantu, która zawiera jeden z kilku typów danych:
struct Variant { union { std::string str; int num; bool b; }; enum Type { Str, Int, Bool } type; };
Jeśli
Variant
instancja trzymała astd::string
, a teraz przypisujesz inny typ do unii, musisz zniszczyćstd::string
pierwszy. Kompilator nie zrobi tego automatycznie .źródło
Mam inną sytuację, w której myślę, że nazwanie destruktora jest całkowicie uzasadnione.
Pisząc metodę typu „Reset” w celu przywrócenia obiektu do jego stanu początkowego, całkowicie uzasadnione jest wywołanie Destructor w celu usunięcia starych danych, które są resetowane.
class Widget { private: char* pDataText { NULL }; int idNumber { 0 }; public: void Setup() { pDataText = new char[100]; } ~Widget() { delete pDataText; } void Reset() { Widget blankWidget; this->~Widget(); // Manually delete the current object using the dtor *this = blankObject; // Copy a blank object to the this-object. } };
źródło
cleanup()
metodę do wywołania w tym przypadku i w destruktorze?