2010-06-17 5 views
50

Является ли эта программа хорошо определенной, а если нет, то почему именно?Может ли деструктор быть рекурсивным?

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

Мои рассуждения: хотя invoking a destructor twice is undefined behavior, на 12,4/14, что он говорит именно это:

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

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

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

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

+1

Это действительно странно, почему вы когда-нибудь хотите назвать рекурсивный деструктор? – Andrey

+0

Достойный вопрос для дяди Боба. –

+3

Почему, черт возьми, ты когда-нибудь захочешь это сделать? – Puppy

ответ

58

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

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

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

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

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

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

+0

@James: Не связанный вопрос, но где я могу скачать/просмотреть документацию стандарта? – Jacob

+4

D'oh, не проверял определение * срока жизни *. Это звучит странно, потому что это означает, что во время обычного вызова деструктора я обращаюсь к членам объекта, срок жизни которого закончился. Но если это так, то это должно быть правдой. – Cubbi

+0

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

1

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

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

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

9

Хорошо, мы понимали, что поведение не определено. Но давайте сделаем небольшое путешествие в то, что действительно происходит. Я использую 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 http://img638.imageshack.us/img638/8508/dest.png

Что такое, что scalar deleting destructor? Это то, что компилятор вставляет между удалением и нашим фактическим кодом. Сам деструктор - это всего лишь метод, в нем нет ничего особенного. Это не освобождает память. Он выпущен где-то внутри этого 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), можно рекурсивно вызывать деструктор. Но все же это не очень хорошая идея, ИМО.

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

+4

Это все еще не является даже дистанционно безопасным: хотя деструктор является «просто» функцией-членом, каждый раз, когда он возвращает его, он вызывает деструкторы любых базовых классов и переменных-членов (это легко проверить). –

+10

Тестирование вывода чего-либо не является показателем законности любого бита кода. То, что вы получаете, в лучшем случае, - это определенное поведение определенного компилятора на определенном фрагменте кода, но это не имеет ничего общего с C++ как языком. – GManNickG

+1

+1 для тестирования. Я все еще не могу представить, как вы можете иметь такую ​​идею. – neuro

5

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

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

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

+4

+1 для предложения альтернативы, совместимой с стандартами. –

+0

Я не вижу, какую функциональность можно было бы сделать из самого корпуса dtor - как по стандарту, так и на практике. Вы находитесь в * точном * том же месте относительно времени жизни объекта. Например, вы должны быть очень осторожны, чтобы не вызывать виртуальные функции из этого разделенного рекурсивного вызова, поскольку вы получили бы неопределенное поведение. –

+0

Деструктор не является нормальной функцией: вы не должны вызывать деструктор напрямую: он должен быть вызван «кодом рамки», который управляет освобождением объекта. Означает ли это, что деструктор может или не может называть себя рекурсивно, является, насколько мне известно, нечетко определенным в спецификациях языка. Возможно, что компилятор может реализовать деструкторы таким образом, чтобы рекурсивный вызов не работал, например. это может преждевременно освободить объект. Поэтому я хочу сказать: зачем рисковать? (продолжение) – Jay

0

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

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

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