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

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

#include 
#include 
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, что это говорит, точно это:

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

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

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

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

50
задан Community 23 May 2017 в 02:32
поделиться

4 ответа

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

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

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

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

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

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

60
ответ дан 7 November 2019 в 11:02
поделиться

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

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

Давайте перейдем к деструктору удаления скаляров и посмотрим на разборку:

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, поскольку деструктор - это просто метод, и весь код освобождения памяти вводится в среднюю функцию ( деструктор удаления скаляра ), рекурсивный вызов деструктора является безопасным. Но все же это плохая идея, ИМО.

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

9
ответ дан 7 November 2019 в 11:02
поделиться

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

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

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

1
ответ дан 7 November 2019 в 11:02
поделиться

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

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

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

5
ответ дан 7 November 2019 в 11:02
поделиться
Другие вопросы по тегам:

Похожие вопросы: