Czy Destruktor może być rekurencyjny?

Czy ten program jest dobrze zdefiniowany, a jeśli nie, to dlaczego dokładnie?

#include <iostream>
#include <new>
struct X {
    int cnt;
    X (int i) : cnt(i) {}
    ~X() {  
            std::cout << "destructor called, cnt=" << cnt << std::endl;
            if ( cnt-- > 0 )
                this->X::~X(); // explicit recursive call to dtor
    }
};
int main()
{   
    char* buf = new char[sizeof(X)];
    X* p = new(buf) X(7);
    p->X::~X();  // explicit call to dtor
    delete[] buf;
}

Moje rozumowanie: chociaż wywołanie destruktora dwa razy jest niezdefiniowanym zachowaniem, na 12.4 / 14, to co dokładnie mówi Jest Takie:

Zachowanie jest niezdefiniowane, jeśli Destruktor jest wywoływany dla obiektu którego życie się skończyło

Co nie wydaje się zabraniać wywołań rekurencyjnych. Podczas gdy Destruktor obiektu jest wykonywany, jego żywotność jeszcze się nie skończyła, więc nie jest UB, by ponownie wywołać Destruktor. Z drugiej strony 12,4 / 6 mówi:

Po wykonaniu ciała [...] a destruktor dla klasy X wywołuje destruktory dla członków bezpośrednich X, destruktory dla bazy bezpośredniej X klasy [...]

Co oznacza, że po powrocie z rekurencyjnego wywołania destruktora, wszystkie destruktory klasy member I base zostaną wywołane, a wywołanie ich ponownie po powrocie do poprzedniego poziomu rekurencji będzie UB. Dlatego Klasa bez bazy i tylko członkowie POD mogą mieć Destruktor rekurencyjny bez UB. Mam rację?

Author: Community, 2010-06-17

5 answers

Odpowiedź brzmi nie, ze względu na definicję "życia" w §3.8/1:

Czas Życia obiektu typu T kończy się, gdy:

- jeśli T jest typem klasy z nietrywialnym destruktorem( 12.4), wywołanie destruktora zostanie wywołane, lub

- magazyn, który zajmuje obiekt, jest ponownie używany lub zwolniony.

Gdy tylko Destruktor zostanie wywołany (za pierwszym razem), czas życia obiektu się kończy. Tak więc, jeśli wywołamy destruktor dla obiektu z w destruktorze zachowanie jest niezdefiniowane, zgodnie z §12.4 / 6:

Zachowanie jest niezdefiniowane, jeśli Destruktor jest wywoływany dla obiektu, którego żywotność się skończyła

 58
Author: James McNellis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-06-17 16:01:50

Ok, zrozumieliśmy, że zachowanie nie jest zdefiniowane. Ale zróbmy małą podróż do tego, co naprawdę się dzieje. Używam VS 2008.

Oto Mój kod:

class Test
{
int i;

public:
    Test() : i(3) { }

    ~Test()
    {
        if (!i)
            return;     
        printf("%d", i);
        i--;
        Test::~Test();
    }
};

int _tmain(int argc, _TCHAR* argv[])
{
    delete new Test();
    return 0;
}

Uruchomimy go i ustawmy punkt przerwania wewnątrz destruktora i niech zdarzy się cud rekurencji.

Oto stack trace:

Alt text http://img638.imageshack.us/img638/8508/dest.png

Co to jest scalar deleting destructor? Jest to coś, co kompilator wstawia między delete a naszym rzeczywistym kodem. Destructor sama w sobie jest tylko metodą, nie ma w tym nic specjalnego. To nie zwalnia pamięci. Jest uwalniany gdzieś wewnątrz scalar deleting destructor.

Przejdźmy do scalar deleting destructor i przyjrzyjmy się demontażowi:

01341580  mov         dword ptr [ebp-8],ecx 
01341583  mov         ecx,dword ptr [this] 
01341586  call        Test::~Test (134105Fh) 
0134158B  mov         eax,dword ptr [ebp+8] 
0134158E  and         eax,1 
01341591  je          Test::`scalar deleting destructor'+3Fh (134159Fh) 
01341593  mov         eax,dword ptr [this] 
01341596  push        eax  
01341597  call        operator delete (1341096h) 
0134159C  add         esp,4 

Podczas rekurencji utknęliśmy pod adresem 01341586, a pamięć jest w rzeczywistości zwalniana tylko pod adresem 01341597.

Wniosek: W VS 2008, ponieważ Destruktor jest tylko metodą, a cały kod uwalniania pamięci jest wtryskiwany do środkowej funkcji (scalar deleting destructor), można bezpiecznie wywołać Destruktor rekurencyjnie. Ale nadal to nie jest dobry pomysł, IMO.

Edit: Ok, ok. Jedyną ideą tej odpowiedzi było przyjrzenie się temu, co się dzieje, gdy wywołujesz Destruktor rekurencyjnie. Ale nie rób tego, ogólnie nie jest to bezpieczne.

 9
Author: Andrey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-06-21 09:57:58

Wraca do definicji kompilatora życia obiektu. Kiedy pamięć jest naprawdę rozdzielona. Myślę, że to możliwe dopiero po zakończeniu destruktora, ponieważ Destruktor ma dostęp do danych obiektu. Dlatego oczekiwałbym, że wywołania rekurencyjne do destruktora zadziałają.

Ale ... z pewnością istnieje wiele sposobów na wdrożenie destruktora i uwolnienie pamięci. Nawet gdyby działało tak, jak chciałem na kompilatorze, którego dziś używam, byłbym bardzo ostrożne poleganie na takim zachowaniu. Istnieje wiele rzeczy, w których dokumentacja mówi, że nie zadziała lub Wyniki są nieprzewidywalne, że w rzeczywistości działają dobrze, jeśli rozumiesz, co naprawdę dzieje się w środku. Ale poleganie na nich jest złą praktyką, chyba że naprawdę musisz, ponieważ jeśli specyfikacje mówią, że to nie działa, to nawet jeśli naprawdę działa, nie masz pewności, że będzie nadal działać w następnej wersji kompilatora.

To powiedziane, jeśli naprawdę chcesz aby wywołać Destruktor rekurencyjnie, a to nie jest tylko hipotetyczne pytanie, dlaczego po prostu nie rozerwać całego ciała destruktora na inną funkcję, niech Destruktor wywoła to, a następnie niech to samo wywołuje rekurencyjnie? To powinno być bezpieczne.

 5
Author: Jay,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-06-17 17:06:09

Tak, to brzmi dobrze. Myślę, że po zakończeniu wywołania destruktora pamięć zostanie z powrotem wrzucona do puli alokowanej, umożliwiając zapisanie czegoś nad nią, co potencjalnie może powodować problemy z kolejnymi wywołaniami destruktora (wskaźnik' this ' byłby nieprawidłowy).

Jednakże, jeśli Destruktor nie zakończy się, dopóki pętla rekurencyjna nie zostanie rozwinięta.. teoretycznie powinno być dobrze.

Ciekawe pytanie:)

 1
Author: Stefan Valianu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-06-17 16:00:32

Dlaczego ktokolwiek chciałby wywołać Destruktor rekurencyjnie w ten sposób ? Po wywołaniu destruktora, powinien zniszczyć obiekt. Jeśli nazwiecie to jeszcze raz, próbowalibyście zainicjować zniszczenie już częściowo zniszczonego obiektu, kiedy jeszcze w tym samym czasie próbowalibyście go zniszczyć.

Wszystkie przykłady mają jakiś malejący / Przyrostowy warunek końcowy, zasadniczo odliczać w rozmowach, co sugeruje jakiś rodzaj nieudanej implementacji zagnieżdżonych klas, które zawierają członków tego samego typu co ona sama.

Dla takiej zagnieżdżonej klasy matryoshka, wywołując destructor na członach, rekurencyjnie, tzn. destructor wywołuje destructor na członie A, który z kolei wywołuje destructor na własnym członie A, który z kolei wywołuje detructor ... i tak dalej jest doskonale w porządku i działa dokładnie tak, jak można się spodziewać. Jest to rekurencyjne użycie destruktora, ale jest to , a nie rekurencyjnie wywołanie destruktora na sobie, co jest szalone i prawie nie ma sensu.

 0
Author: jam spandex,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-05 23:57:16