Czy to dozwolone?

232

Czy jest dozwolone, delete this;jeśli instrukcja delete jest ostatnią instrukcją, która zostanie wykonana w tej instancji klasy? Oczywiście jestem pewien, że obiekt reprezentowany przez this-pointer został newutworzony.

Myślę o czymś takim:

void SomeModule::doStuff()
{
    // in the controller, "this" object of SomeModule is the "current module"
    // now, if I want to switch over to a new Module, eg:

    controller->setWorkingModule(new OtherModule());

    // since the new "OtherModule" object will take the lead, 
    // I want to get rid of this "SomeModule" object:

    delete this;
}

Mogę to zrobić?

Martijn Courteaux
źródło
13
Głównym problemem byłoby to, że delete thisutworzyłeś ścisłe powiązanie między klasą a metodą alokacji używaną do tworzenia obiektów tej klasy. To bardzo kiepski projekt OO, ponieważ najbardziej fundamentalną rzeczą w OOP jest tworzenie autonomicznych klas, które nie wiedzą ani nie dbają o to, co robi ich rozmówca. Dlatego właściwie zaprojektowana klasa nie powinna wiedzieć ani przejmować się tym, jak została przydzielona. Jeśli z jakiegoś powodu potrzebujesz takiego osobliwego mechanizmu, myślę, że lepszym rozwiązaniem byłoby użycie klasy opakowania wokół klasy rzeczywistej i pozwolenie opakowaniu zająć się przydziałem.
Lundin
Nie możesz tego usunąć setWorkingModule?
Jimmy T.

Odpowiedzi:

238

C ++ FAQ Lite zawiera specjalny wpis na ten temat

Myślę, że ten cytat ładnie podsumowuje

O ile jesteś ostrożny, obiekt może popełnić samobójstwo (usuń to).

JaredPar
źródło
15
Odpowiedni FQA ma również użyteczny komentarz: yosefk.com/c++fqa/heap.html#fqa-16.15
Alexandre C.
1
Dla bezpieczeństwa możesz użyć prywatnego destruktora na oryginalnym obiekcie, aby upewnić się, że nie jest on zbudowany na stosie lub jako część tablicy lub wektora.
Cem Kalyoncu
Zdefiniuj „ostrożny”
CinCout,
3
„Ostrożność” jest zdefiniowana w linkowanym artykule FAQ. (Podczas gdy łącze FQA głównie rantuje - jak prawie wszystko w nim - jak złe jest C ++)
CharonX
88

Tak, delete this;ma zdefiniowane wyniki, o ile (jak zauważyłeś) zapewniasz, że obiekt został przydzielony dynamicznie i (oczywiście) nigdy nie próbujesz użyć obiektu po jego zniszczeniu. Z biegiem lat zadawano wiele pytań na temat tego, co konkretnie mówi standard delete this;, a nie usuwania innych wskaźników. Odpowiedź na to pytanie jest dość krótka i prosta: niewiele mówi. Mówi tylko, że deleteoperand musi być wyrażeniem oznaczającym wskaźnik do obiektu lub tablicę obiektów. Wchodzi on w dość szczegółowe informacje na temat takich rzeczy, jak to, jak wymyśla, jaką (jeśli w ogóle) funkcję dealokacji należy wywołać w celu zwolnienia pamięci, ale cała sekcja delete(§ [wykr. Usuń]) w ogóle nie wspomina delete this;konkretnie. W części poświęconej niszczycielom wspomnianodelete this w jednym miejscu (§ [class.dtor] / 13):

W punkcie definicji wirtualnego niszczyciela (w tym niejawnej definicji (15.8)), funkcja niealokacji dealokacji jest określana tak, jakby dla wyrażenia usunąć to pojawiające się w nie-wirtualnym niszczycielu klasy niszczyciela (patrz 8.3.5 ).

Zwykle potwierdza to pogląd, który standard uważa delete this;za prawidłowy - gdyby był nieprawidłowy, jego typ nie miałby znaczenia. delete this;O ile mi wiadomo , to jedyne miejsce, o którym wspomina standard .

W każdym razie niektórzy uważają delete thispaskudny hack i mówią każdemu, kto będzie słuchać, że należy tego unikać. Jednym z najczęściej cytowanych problemów jest trudność zapewnienia, że ​​obiekty klasy są przydzielane tylko dynamicznie. Inni uważają to za całkowicie rozsądny idiom i używają go przez cały czas. Osobiście jestem gdzieś pośrodku: rzadko go używam, ale nie wahaj się, gdy będzie to odpowiednie narzędzie do pracy.

Zasadniczo używasz tej techniki z przedmiotem, który ma życie prawie całkowicie własne. Jednym z przykładów, które zacytował James Kanze, był system rozliczeń / śledzenia, nad którym pracował dla firmy telefonicznej. Kiedy zaczynasz dzwonić, coś bierze to pod uwagę i tworzy phone_callobiekt. Od tego momentu phone_callobiekt obsługuje szczegóły połączenia telefonicznego (nawiązywanie połączenia podczas wybierania numeru, dodawanie pozycji do bazy danych, aby powiedzieć, kiedy połączenie zostało nawiązane, być może połączyć więcej osób, jeśli wykonujesz połączenie konferencyjne itp.) kiedy ostatnie osoby w trakcie połączenia się rozłączają, phone_callobiekt dokonuje ostatecznej księgowości (np. dodaje pozycję do bazy danych, aby powiedzieć, kiedy się rozłączyłeś, aby mogli obliczyć, jak długo trwało twoje połączenie), a następnie sam się niszczy. Żywotnośćphone_callobiekt opiera się na tym, kiedy pierwsza osoba rozpoczyna połączenie, a kiedy ostatnie osoby opuszczają połączenie - z punktu widzenia reszty systemu jest to w zasadzie całkowicie arbitralne, więc nie można powiązać go z żadnym zakresem leksykalnym w kodzie lub cokolwiek w tym zamówieniu.

Dla każdego, kto może dbać o niezawodność tego rodzaju kodowania: jeśli zadzwonisz do, z lub przez prawie dowolną część Europy, istnieje spora szansa, że ​​zostanie to obsłużone (przynajmniej częściowo) za pomocą kodu to robi dokładnie to.

Jerry Coffin
źródło
2
Dzięki, zapisam to gdzieś w pamięci. Przypuszczam, że definiujesz konstruktory i destruktory jako prywatne i używasz statycznej metody fabryki do tworzenia takich obiektów.
Alexandre C.
@Alexandre: Prawdopodobnie i tak byś to zrobił w większości przypadków - nie znam nigdzie blisko wszystkich szczegółów systemu, nad którym pracował, więc nie mogę tego powiedzieć na pewno.
Jerry Coffin
Często omijam problem przydzielania pamięci przez włączenie bool selfDeletekonstruktora, który zostaje przypisany do zmiennej składowej. To prawda, że ​​oznacza to przekazanie programatorowi wystarczającej ilości liny, aby zawiązać w nim pętlę, ale uważam, że jest to lepsze niż wycieki pamięci.
MBraedley,
1
@MBraedley: Zrobiłem to samo, ale wolę unikać tego, co wydaje mi się kludge.
Jerry Coffin
Dla każdego, kogo to obchodzi ... istnieje spora szansa, że ​​jest obsługiwany (przynajmniej częściowo) przez kod, który dokładnie to robi this. Tak, kod jest obsługiwany przez dokładnie this. ;)
Galaxy
46

Jeśli cię to przeraża, istnieje całkowicie legalny hack:

void myclass::delete_me()
{
    std::unique_ptr<myclass> bye_bye(this);
}

Myślę, że delete thisjest to idiomatyczny C ++ i przedstawiam to tylko jako ciekawość.

Istnieje przypadek, w którym ta konstrukcja jest rzeczywiście przydatna - możesz usunąć obiekt po zgłoszeniu wyjątku, który wymaga danych elementu z obiektu. Obiekt pozostaje ważny do momentu wykonania rzutu.

void myclass::throw_error()
{
    std::unique_ptr<myclass> bye_bye(this);
    throw std::runtime_exception(this->error_msg);
}

Uwaga: jeśli używasz kompilatora starszego niż C ++ 11, którego możesz użyć std::auto_ptrzamiast std::unique_ptr, zrobi to samo.

Mark Ransom
źródło
Nie mogę tego skompilować przy użyciu c ++ 11, czy są jakieś specjalne opcje kompilatora? Czy nie wymaga również przesunięcia tego wskaźnika?
Owl
@ Nie jestem pewien, co masz na myśli, to działa dla mnie: ideone.com/aavQUK . Tworzenie unique_ptrz innego unique_ptr wymaga ruchu, ale nie z surowego wskaźnika. Chyba, że ​​coś się zmieni w C ++ 17?
Mark Ransom
Ahh C ++ 14, właśnie dlatego. Muszę zaktualizować moje c ++ na moim dev. Spróbuję jeszcze dziś wieczorem na moim niedawno powstałym systemie gentoo!
Sowa
25

Jednym z powodów zaprojektowania C ++ było ułatwienie ponownego użycia kodu. Ogólnie C ++ powinien być napisany tak, aby działał niezależnie od tego, czy klasa jest tworzona na stercie, w tablicy, czy na stosie. „Usuń to” jest bardzo złą praktyką kodowania, ponieważ zadziała tylko wtedy, gdy na stercie zostanie zdefiniowana pojedyncza instancja; i lepiej, żeby nie było innej instrukcji usuwania, która jest zwykle używana przez większość programistów do czyszczenia sterty. Robi to również przy założeniu, że żaden programista konserwacji w przyszłości nie wyleczy fałszywie postrzeganego wycieku pamięci poprzez dodanie instrukcji delete.

Nawet jeśli wiesz z góry, że Twoim obecnym planem jest przydzielenie tylko jednej instancji na stosie, co się stanie, jeśli jakiś przyszły programista pojawi się w przyszłości i zdecyduje się utworzyć instancję na stosie? Albo co, jeśli pokroi i wklei pewne części klasy do nowej klasy, którą zamierza wykorzystać na stosie? Kiedy kod osiągnie „usuń to”, to on zgaśnie i usunie go, ale wtedy, gdy obiekt wykroczy poza zakres, wywoła destruktor. Destruktor spróbuje go ponownie usunąć, a następnie zostaniesz powstrzymany. W przeszłości zrobienie czegoś takiego zepsułoby nie tylko program, ale także system operacyjny i komputer wymagałyby ponownego uruchomienia. W każdym razie jest to zdecydowanie NIE zalecane i prawie zawsze należy tego unikać. Musiałbym być zdesperowany, poważnie otynkowany,

Bob Bryan
źródło
7
+1. Nie rozumiem, dlaczego zostałeś odrzucony. „C ++ powinno być napisane tak, aby działało bez względu na to, czy instancja klasy jest tworzona na stercie, w tablicy, czy na stosie” jest bardzo dobrą radą.
Joh
1
Możesz po prostu owinąć obiekt, który chcesz usunąć, w specjalną klasę, która usuwa obiekt, a następnie siebie, i użyj tej techniki, aby zapobiec alokacji stosu: stackoverflow.com/questions/124880/... Są chwile, kiedy tak naprawdę nie ma realna alternatywa. Właśnie użyłem tej techniki do samodzielnego usunięcia wątku, który jest uruchamiany przez funkcję DLL, ale funkcja DLL musi wrócić przed zakończeniem wątku.
Felix Dombek
Nie możesz programować w taki sposób, że ktoś, kto po prostu kopiuje i wkleja twój kod, ostatecznie go niewłaściwie używa
Jimmy T.
22

Jest to dozwolone (po prostu nie używaj później tego obiektu), ale nie napisałbym takiego kodu w praktyce. Myślę, że delete thispowinien pojawić się tylko w funkcji, które nazywa się releasealbo Releasei wygląda następująco: void release() { ref--; if (ref<1) delete this; }.

Kirill V. Lyadvinsky
źródło
Który jest dokładnie raz w każdym moim projekcie ... :-)
cmaster - przywróć monikę
15

Cóż, w modelu Component Object Model (COM) delete thiskonstrukcja może być częścią Releasemetody, która jest wywoływana za każdym razem, gdy chcesz zwolnić obiekt akwizytowany:

void IMyInterface::Release()
{
    --instanceCount;
    if(instanceCount == 0)
        delete this;
}
Nieznany Gosu
źródło
8

To jest podstawowy idiom dla obiektów zliczanych przez referencje.

Liczenie referencji jest silną formą deterministycznego zbierania śmieci - zapewnia, że ​​obiekty zarządzają swoim własnym czasem życia, zamiast polegać na „inteligentnych” wskaźnikach itp. Dostęp do obiektu bazowego jest zawsze możliwy tylko poprzez inteligentne wskaźniki „Referencyjne”, zaprojektowane w taki sposób, aby wskaźniki zwiększały i zmniejszały liczbę całkowitą elementu (liczbę referencji) w rzeczywistym obiekcie.

Kiedy ostatnie odniesienie spadnie ze stosu lub zostanie usunięte, liczba odniesień spadnie do zera. Domyślnym zachowaniem Twojego obiektu będzie wówczas wezwanie do „usunięcia tego” w celu wyrzucania elementów bezużytecznych - biblioteki, które piszę, zapewniają chronione wirtualne wywołanie „CountIsZero” w klasie podstawowej, dzięki czemu możesz nadpisać to zachowanie w przypadku buforowania.

Kluczem do uczynienia tego bezpiecznym nie jest umożliwienie użytkownikom dostępu do KONSTRUKTORA danego obiektu (uczynienie go chronionym), ale zamiast tego wywołują jakiegoś statycznego członka - FABRYCZNĄ „statyczną referencję CreateT (...)”. W ten sposób WIESZ na pewno, że zawsze są one zbudowane ze zwykłym „nowym” i że nigdy nie jest dostępny żaden surowy wskaźnik, więc „usuń to” nigdy nie wybuchnie.

Zack Yezek
źródło
Dlaczego nie można mieć po prostu „urządzenia przydzielającego / śmieciowego” klasy (singleton), interfejsu, przez który odbywa się wszystkie przydzielanie, i pozwolić tej klasie na obsługę liczenia referencji przydzielonych obiektów? Zamiast zmuszać same obiekty do kłopotania się zadaniami wywozu śmieci, jest to coś zupełnie niezwiązanego z ich przeznaczeniem.
Lundin
1
Możesz także po prostu zabezpieczyć destruktor, aby zabezpieczyć przydziały statyczne i stosowe obiektu.
Game_Overture
7

Możesz to zrobić. Nie możesz jednak do tego przypisać. Dlatego powód, dla którego to robisz: „Chcę zmienić pogląd”, wydaje się bardzo wątpliwy. Moim zdaniem lepszym sposobem byłoby zastąpienie tego obiektu przez obiekt, który trzyma widok.

Oczywiście używasz obiektów RAII, więc nie musisz wcale w ogóle wywoływać usuwania ... prawda?

Edward Strange
źródło
4

To stare pytanie, na które udzielono odpowiedzi, ale @Alexandre zapytał „Dlaczego ktoś miałby to zrobić?” I pomyślałem, że mogę podać przykładowe zastosowanie, które rozważam tego popołudnia.

Starszy kod. Używa nagich wskaźników Obj * obj z usuwaniem obj na końcu.

Niestety czasami potrzebuję, nie często, utrzymać obiekt przy życiu dłużej.

Zastanawiam się, czy to inteligentny wskaźnik liczony jako odniesienie. Ale byłoby wiele kodu do zmiany, gdybym miał używać go ref_cnt_ptr<Obj>wszędzie. A jeśli wymieszasz nagiego Obj * i ref_cnt_ptr, możesz niejawnie usunąć obiekt, gdy ostatni ref_cnt_ptr zniknie, nawet jeśli Obj * wciąż żyje.

Zastanawiam się więc nad stworzeniem jawnego_delete_ref_cnt_ptr. Tj. Wskaźnik zliczający odniesienie, w którym usuwanie odbywa się tylko w jawnej procedurze usuwania. Używanie go w jednym miejscu, w którym istniejący kod zna czas życia obiektu, a także w moim nowym kodzie, który utrzymuje obiekt dłużej.

Zwiększanie i zmniejszanie liczby odwołań, gdy manipulujesz jawnie_delete_ref_cnt_ptr.

Ale NIE zwalnia, gdy liczba referencyjna jest widoczna jako zero w destruktorze jawnie_delete_ref_cnt_ptr.

Uwalnianie tylko wtedy, gdy liczba referencyjna jest widoczna jako zero w jawnej operacji podobnej do usuwania. Np. W czymś takim jak:

template<typename T> class explicit_delete_ref_cnt_ptr { 
 private: 
   T* ptr;
   int rc;
   ...
 public: 
   void delete_if_rc0() {
      if( this->ptr ) {
        this->rc--;
        if( this->rc == 0 ) {
           delete this->ptr;
        }
        this->ptr = 0;
      }
    }
 };

OK, coś takiego. To trochę niezwykłe, że typ wskaźnika liczonego w referencjach nie usuwa automatycznie obiektu wskazanego w rc'ed ptr destructor. Wygląda jednak na to, że mieszanie nagich wskaźników i wskaźników rc może być nieco bezpieczniejsze.

Ale do tej pory nie trzeba tego usuwać.

Ale wtedy przyszło mi do głowy: jeśli obiekt, na który wskazywał, pointee, wie, że jest liczony jako referencja, np. Jeśli liczba znajduje się w obiekcie (lub w innej tabeli), wówczas procedura delete_if_rc0 może być metodą obiekt pointee, a nie (inteligentny) wskaźnik.

class Pointee { 
 private: 
   int rc;
   ...
 public: 
   void delete_if_rc0() {
        this->rc--;
        if( this->rc == 0 ) {
           delete this;
        }
      }
    }
 };

W rzeczywistości nie musi to wcale być metoda składowa, ale może być funkcją bezpłatną:

map<void*,int> keepalive_map;
template<typename T>
void delete_if_rc0(T*ptr) {
        void* tptr = (void*)ptr;
        if( keepalive_map[tptr] == 1 ) {
           delete ptr;
        }
};

(BTW, wiem, że kod nie jest całkiem poprawny - staje się mniej czytelny, jeśli dodam wszystkie szczegóły, więc zostawiam go w ten sposób.)

Krazy Glew
źródło
0

Usuń to jest legalne, dopóki obiekt jest w stercie. Trzeba będzie wymagać, aby obiekt był tylko stertą. Jedynym sposobem, aby to zrobić, jest zabezpieczenie destruktora - w ten sposób usuwanie może być wywoływane TYLKO z klasy, więc potrzebujesz metody, która zapewni usunięcie

Swift - piątek ciasto
źródło