Может ли деструктор быть рекурсивным? - PullRequest
50 голосов
/ 17 июня 2010

Четко ли определена эта программа, и если нет, то почему?

#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;
}

Мое рассуждение: хотя дважды вызывать деструктор - это неопределенное поведение , в соответствии с 12.4 / 14, что именно оно говорит:

поведение не определено, если деструктор вызывается для объекта чья жизнь закончилась

Что, похоже, не запрещает рекурсивные вызовы. В то время как деструктор для объекта выполняется, время жизни объекта еще не закончилось, поэтому не стоит снова вызывать деструктор. С другой стороны, 12.4 / 6 говорит:

После казни тела [...] деструктор для класса X вызывает деструкторы для прямых членов X, деструкторы для прямой базы X классы [...]

, что означает, что после возврата из рекурсивного вызова деструктора будут вызваны все деструкторы члена и базового класса, и их повторный вызов при возврате к предыдущему уровню рекурсии будет UB. Поэтому класс без базы и только члены POD может иметь рекурсивный деструктор без UB. Я прав?

Ответы [ 5 ]

58 голосов
/ 17 июня 2010

Ответ «нет» из-за определения «времени жизни» в §3.8 / 1:

Время жизни объекта типа T заканчивается, когда:

- если T является типом класса с нетривиальным деструктором (12.4), начинается вызов деструктора, или

- хранилище, которое занимает объект, используется повторно или освобождается.

Как только деструктор вызывается (первый раз), срок службы объекта заканчивается. Таким образом, если вы вызываете деструктор для объекта из деструктора, поведение не определено, согласно §12.4 / 6:

поведение не определено, если деструктор вызывается для объекта, время жизни которого закончилось

9 голосов
/ 17 июня 2010

Хорошо, мы поняли, что поведение не определено.Но давайте сделаем небольшое путешествие в то, что действительно происходит.Я использую VS 2008.

Вот мой код:

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;
}

Давайте запустим его и установим точку останова внутри деструктора, и пусть случится чудо рекурсии.

Вот трассировка стека:

альтернативный текст http://img638.imageshack.us/img638/8508/dest.png

Что это такое scalar deleting destructor?Это то, что компилятор вставляет между delete и нашим реальным кодом.Сам по себе деструктор - это просто метод, в этом нет ничего особенного.Это действительно не освобождает память.Это выпущено где-то внутри этого scalar deleting destructor.

Давайте перейдем к scalar deleting destructor и рассмотрим разборку:

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 

во время нашей рекурсии мы застряли по адресу 01341586, и память фактически высвобождается только по адресу01341597.

Заключение: В VS 2008, поскольку деструктор является просто методом, а весь код освобождения памяти вводится в среднюю функцию (scalar deleting destructor), безопасно вызывать деструктор рекурсивно.Но все же это не очень хорошая идея, ИМО.

Редактировать : Хорошо, хорошо.Единственная идея этого ответа состояла в том, чтобы взглянуть на то, что происходит, когда вы вызываете деструктор рекурсивно.Но не делайте этого, это вообще небезопасно.

5 голосов
/ 17 июня 2010

Возвращается к определению компилятором времени жизни объекта.Например, когда память действительно выделена.Я думаю, что это не может быть до тех пор, пока деструктор не завершится, поскольку деструктор имеет доступ к данным объекта.Поэтому я ожидаю, что рекурсивные вызовы деструктора сработают.

Но ... несомненно, есть много способов реализовать деструктор и освободить память.Даже если бы это работало так, как я хотел на компиляторе, который я использую сегодня, я бы очень осторожно относился к такому поведению.Есть много вещей, где документация говорит, что это не будет работать, или результаты непредсказуемы, что на самом деле работает просто отлично, если вы понимаете, что на самом деле происходит внутри.Но это плохая практика - полагаться на них, если только вам это не нужно, потому что если в спецификациях говорится, что это не работает, то даже если это действительно работает, у вас нет уверенности в том, что это продолжит работать в следующей версии.compiler.

Тем не менее, если вы действительно хотите вызывать свой деструктор рекурсивно, и это не просто гипотетический вопрос, почему бы просто не скопировать все тело деструктора в другую функцию, пусть деструктор вызовет это,а потом пусть это будет называть себя рекурсивно?Это должно быть безопасно.

1 голос
/ 17 июня 2010

Да, это звучит примерно так.Я думаю, что после завершения вызова деструктора память будет выгружена обратно в выделяемый пул, что позволит что-то записать поверх него, что потенциально может вызвать проблемы с последующими вызовами деструктора (указатель this будет недействительным)1001 *

Однако, если деструктор не завершает работу до тех пор, пока не размотан рекурсивный цикл ... теоретически все должно быть в порядке.

Интересный вопрос :)

0 голосов
/ 06 августа 2017

Зачем кому-то хотеть рекурсивно вызывать деструктор таким образом?Как только вы вызвали деструктор, он должен уничтожить объект.Если вы вызовете его снова, вы будете пытаться инициировать уничтожение уже частично разрушенного объекта, когда вы все еще на самом деле были частично уничтожили его одновременно.

Все примеры имеют своего родадекрементальное / инкрементное конечное условие, чтобы по существу вести обратный отсчет в вызовах, что наводит на мысль о некоторой неудачной реализации вложенного класса, который содержит члены того же типа, что и он сам.

Для такого вложенного класса matryoshka рекурсивный вызов деструктора для членов, то есть деструктор вызывает деструктор для члена A, который, в свою очередь, вызывает деструктор для своего собственного члена A, который, в свою очередь, вызывает детектор... и так далее совершенно нормально и работает именно так, как и следовало ожидать.Это рекурсивное использование деструктора, но это не рекурсивный вызов деструктора самого себя, который безумен и почти не имеет смысла.

...