Явный вызов деструктора

Я наткнулся на следующий фрагмент кода:

#include <iostream>
#include <string>
using namespace std;
class First
{
    string *s;
    public:
    First() { s = new string("Text");}
    ~First() { delete s;}
    void Print(){ cout<<*s;}
};

int main()
{
    First FirstObject;
    FirstObject.Print();
    FirstObject.~First();
}

В тексте говорилось, что этот фрагмент должен вызвать ошибку времени выполнения. Я не был в этом уверен, поэтому попытался скомпилировать и запустить его. Это сработало. Странно то, что, несмотря на простоту задействованных данных, программа заикалась после печати «Текст» и завершилась только через одну секунду.

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

Простой поиск подтвердил, что явный вызов деструктора для автоматизированного объекта опасен, так как второй вызов (когда объект выходит за пределы области видимости) имеет неопределенное поведение. Так что мне повезло с моим компилятором (VS 2017) или с этой конкретной программой.

Текст просто неверен об ошибке времени выполнения? Или действительно часто возникает ошибка времени выполнения? Или, может быть, мой компилятор реализовал какой-то механизм защиты от подобных вещей?


person Andrea Bocco    schedule 12.11.2018    source источник
comment
Стандарт C++ никогда не гарантирует ошибку времени выполнения (это всегда неопределенное поведение), поэтому текст явно неверен   -  person UnholySheep    schedule 12.11.2018
comment
@UnholySheep, я не уверен, что сказал бы это. Например, исключение, покидающее функцию noexcept, является гарантированным вызовом std::terminate, который я бы классифицировал как ошибку времени выполнения.   -  person chris    schedule 12.11.2018
comment
@chris хорошая мысль, я думал только об описанном случае (и я видел слишком много текстов, утверждающих, что код, который вызывает UB, всегда будет вызывать ошибку времени выполнения / ошибку сегментации)   -  person UnholySheep    schedule 12.11.2018
comment
Как он печатает строку дважды? Он освободит его дважды, потому что деструктор будет вызываться при уничтожении объекта.   -  person Matthieu Brucher    schedule 12.11.2018
comment
Определенно возникает ошибка времени выполнения: деструктор вызывается дважды, это ошибка, и это происходит во время выполнения. Бьюсь об заклад, вы можете поймать его, если запустите тест в режиме отладки. Почему не выскакивает сообщение? Что делает VC2017 в режиме выпуска при завершении работы вашего приложения, зная, что последние две вещи, которые он делает, удаляют один и тот же указатель? Есть ли какая-то оптимизация, которая по ошибке скрывает/исправляет вашу ошибку? Вы, вероятно, должны спросить поддержку MS ...   -  person L.C.    schedule 12.11.2018
comment
@L.C. Возможно, вы захотите дать свой собственный ответ; однако будьте осторожны: вы явно заявляете противоположное тому, что требует Стандарт.   -  person YSC    schedule 12.11.2018
comment
@YSC Я совершенно уверен, что вы согласны с тем, что то, что требует стандарт, и то, что делает компилятор MS, к сожалению, не всегда одно и то же. Я по-прежнему считаю, что двойное удаление указателя является ошибкой, и, поскольку это происходит во время выполнения, это означает, что это ошибка времени выполнения (если я не пропустил определение?). Кроме того, тот факт, что мой отладчик злится, подтверждает мою идею, но опять же, не все отладчики ведут себя одинаково...   -  person L.C.    schedule 12.11.2018
comment
@L.C. Я делаю. И, в частности, приветствуется технический ответ о msvc. Если это хорошо, я постараюсь быть первым, кто проголосует.   -  person YSC    schedule 12.11.2018
comment
@MatthieuBrucher Я думаю, они добавили оператор печати в деструктор.   -  person Solomon Ucko    schedule 12.11.2018
comment
Причина, по которой сообщение не появляется сразу, вероятно, заключается в том, что вы не очищаете буфер cout. Вам нужно вызвать cout ‹‹ std::flush, чтобы очистить буфер (или cout ‹‹ std::endl, который вызывает flush).   -  person Aaron Queenan    schedule 13.11.2018
comment
@MatthieuBrucher освобождение и уничтожение - разные вещи. Обычно они возникают одновременно, но бывают условия, когда этого не происходит.   -  person Mark Ransom    schedule 19.11.2018
comment
Я знаю? Я не думаю, что сказал что-то, чтобы указать на обратное.   -  person Matthieu Brucher    schedule 19.11.2018
comment
@MatthieuBrucher, вы сказали, что он освободит его дважды. Но теперь, оглядываясь назад, вы говорили о переменной-члене s, а не о самом объекте. Виноват.   -  person Mark Ransom    schedule 20.11.2018
comment
@markeransom, не беспокойтесь   -  person Matthieu Brucher    schedule 20.11.2018
comment
@Andrea Bocco Если ответ вас устраивает, примите его, нажав на галочку под его оценкой. Я вижу, вы задали 9 вопросов по SO и не ответили ни на один. Возможно, вы захотите вернуться к ним и принять ответы, которые, по вашему мнению, решили ваши проблемы. Это важно, потому что именно так работает StackOverflow.   -  person YSC    schedule 22.11.2018
comment
Привет, Адреа. Я заметил, что вы не приняли ни один из ответов на ваши вопросы. Может быть, вы не знаете, как это работает. Когда вы рассматриваете ответ для решения вашей проблемы/ответа на свой вопрос/и т. д., вы можете щелкнуть галочку под оценкой ответа, чтобы продвигать его как принятый ответ. Он дает 15реп своему автору и 2реп вам. Подробнее о принятом ответе здесь: Что это значит, когда ответ принят?.   -  person YSC    schedule 24.12.2018


Ответы (3)


Простой поиск подтвердил, что явный вызов деструктора для автоматизированного объекта опасен, так как второй вызов (когда объект выходит за пределы области видимости) имеет неопределенное поведение.

Это правда. Неопределенное поведение вызывается, если вы явно уничтожаете объект с автоматическим сохранением. Подробнее об этом.

Так что мне повезло с моим компилятором (VS 2017) или с этой конкретной программой.

Я бы сказал, что вам не повезло. Лучшее (для вас, кодера), что может случиться с UB, — это сбой при первом запуске. Если он работает нормально, сбой может произойти 19 января 2038 года в рабочей среде.

Текст просто неверен об ошибке времени выполнения? Или действительно часто возникает ошибка времени выполнения? Или, может быть, мой компилятор реализовал какой-то механизм защиты от подобных вещей?

Да, текст какой-то неправильный. Неопределенное поведение не определено. Ошибка во время выполнения — это только одна из многих возможностей (включая носовых демонов).

Полезно прочитать о неопределенном поведении: Что такое неопределенное поведение?

person YSC    schedule 12.11.2018
comment
Неопределенное поведение вызывается, когда вы явно уничтожаете объект с автоматическим сохранением. Скорее, оно вызывается, когда объект достигает конца своего естественного жизненного цикла без повторного воплощения. Сам по себе вызов деструктора не является UB. - person StoryTeller - Unslander Monica; 12.11.2018
comment
Точнее, 03:14:08 UTC 19 января 2038 года :D - person George Spatacean; 12.11.2018
comment
@StoryTeller Это правда. Я хотел избежать конкретики. Я исправил это. - person YSC; 12.11.2018

Нет, это просто неопределенное поведение из проекта стандарта C++ [class.dtor]p16:

Как только для объекта вызывается деструктор, этот объект больше не существует; поведение не определено, если деструктор вызывается для объекта, время жизни которого закончилось ([basic.life]). [ Пример: если деструктор для автоматического объекта вызывается явно, а затем блок покидает таким образом, который обычно вызывал бы неявное уничтожение объекта, поведение не определено. — конечный пример

и мы можем видеть из определения неопределенного поведения:

поведение, для которого этот документ не устанавливает никаких требований

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

Кроме того, [class.dtor]p15 дает больше контекста в нормативном разделе, который я цитирую. над:

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

void* operator new(std::size_t, void* p) { return p; }
struct X {
  X(int);
  ~X();
};
void f(X* p);

void g() {                      // rare, specialized use:
  char* buf = new char[sizeof(X)];
  X* p = new(buf) X(222);       // use buf[] and initialize
  f(p);
  p->X::~X();                   // cleanup
}

— конец примечания ]

person Shafik Yaghmour    schedule 12.11.2018

Текст просто неверен об ошибке времени выполнения?

Это неверно.

Или действительно часто возникает ошибка времени выполнения? Или, может быть, мой компилятор реализовал какой-то механизм защиты от подобных вещей?

Вы не можете этого знать, и именно это происходит, когда ваш код вызывает Undefined Behavior< /а>; вы не знаете, что произойдет, когда вы его выполните.

В вашем случае вам (не)повезло*, и это сработало, а у меня это вызвало ошибка (двойной бесплатный).


*Потому что, если бы вы получили ошибку, вы бы начали отладку, иначе, например, в большом проекте, вы могли бы ее пропустить...

person gsamaras    schedule 12.11.2018