Действительно ли эта программа четко определена, и в противном случае почему точно?
#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.Я прав?
Ответ отрицательный, из-за определения «времени жизни» в §3.8 / 1:
Время жизни объекта типа
T
заканчивается when:- если
T
- это тип класса с нетривиальным деструктором (12.4), запускается вызов деструктора, или- память, которую занимает объект, повторно используется или освобождается.
Как только деструктор вызывается (в первый раз), время жизни объекта заканчивается. Таким образом, если вы вызываете деструктор для объекта из деструктора, поведение не определено, согласно §12.4 / 6:
поведение не определено, если деструктор вызывается для объекта, время жизни которого закончилось
Хорошо, мы поняли, что поведение не определено. Но давайте сделаем небольшое путешествие в то, что происходит на самом деле. Я использую 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;
}
Давайте запустим его, установим точку останова внутри деструктора и позволим чуду рекурсии случиться.
Вот трассировка стека:
Что это за скалярный деструктор удаления
? Это то, что компилятор вставляет между удалением и нашим фактическим кодом. Сам деструктор - это просто метод, в нем нет ничего особенного. На самом деле это не освобождает память. Он выпущен где-то внутри этого скалярного деструктора удаления
.
Давайте перейдем к деструктору удаления скаляров
и посмотрим на разборку:
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, поскольку деструктор - это просто метод, и весь код освобождения памяти вводится в среднюю функцию ( деструктор удаления скаляра
), рекурсивный вызов деструктора является безопасным. Но все же это плохая идея, ИМО.
Редактировать : Хорошо, хорошо. Единственная идея этого ответа состояла в том, чтобы взглянуть на то, что происходит, когда вы рекурсивно вызываете деструктор. Но не делайте этого, в целом это небезопасно.
Да, звучит примерно так. Я бы подумал, что после завершения вызова деструктора память будет сброшена обратно в пул выделения, что позволит чему-то записать ее, что потенциально может вызвать проблемы с последующими вызовами деструктора (указатель 'this' будет недействительным).
Однако, если деструктор не завершается до тех пор, пока рекурсивный цикл не будет развернут... теоретически, все должно быть в порядке.
Интересный вопрос :)
Это возвращается к определению компилятором времени жизни объекта. Например, когда действительно освобождается память. Я бы подумал, что этого не может быть до завершения деструктора, поскольку деструктор имеет доступ к данным объекта. Поэтому я ожидаю, что рекурсивные вызовы деструктора сработают.
Но ... несомненно, есть много способов реализовать деструктор и освободить память. Даже если бы он работал так, как я хотел, на компиляторе, который я использую сегодня, я был бы очень осторожен, полагаясь на такое поведение. Есть много вещей, о которых в документации говорится, что это не сработает или результаты непредсказуемы, но на самом деле все работает нормально, если вы понимаете, что на самом деле происходит внутри. Но полагаться на них - плохая практика, потому что если в спецификациях сказано, что это не работает, то даже если это действительно работает, у вас нет гарантии, что это продолжит работать в следующей версии компилятор.
Тем не менее, если вы действительно хотите вызвать свой деструктор рекурсивно, и это не просто гипотетический вопрос, почему бы просто не скопировать все тело деструктора в другую функцию, позволить деструктору вызвать это, а затем позволить этому вызову сам рекурсивно? Это должно быть безопасно.