Подтвердить что ты не робот

Может ли деструктор быть рекурсивным?

Является ли эта программа корректной, а если нет, то почему именно?

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

Ответ 2

Хорошо, мы поняли, что поведение не определено. Но давайте сделаем небольшое путешествие в то, что действительно происходит. Я использую 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;
}

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

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

alt text

Что это 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), безопасно вызывать деструктор рекурсивно. Но, тем не менее, это не очень хорошая идея, ИМО.

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

Ответ 3

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

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

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

Ответ 4

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

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

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

Ответ 5

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

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

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