Почему не неопределенное поведение уничтожать объект, который был перезаписан путем размещения нового? - PullRequest
0 голосов
/ 03 сентября 2018

Я пытаюсь выяснить, является ли следующее поведение неопределенным. Я чувствую, что это не UB, но мое чтение стандарта делает его похожим на UB:

#include <iostream>

struct A {
    A() { std::cout << "1"; }
    ~A() { std::cout << "2"; }
};

int main() {
    A a;
    new (&a) A;
}

Цитирование стандарта C ++ 11:

basic.life¶4 говорит: «Программа может закончить время жизни любого объекта, повторно используя хранилище, которое этот объект занимает»

Таким образом, после new (&a) A исходный объект A завершил свою жизнь.

class.dtor¶11.3 говорит, что «деструкторы неявно вызываются для построенных объектов с автоматической продолжительностью хранения ([basic.stc.auto]), когда выходит блок, в котором создается объект ([stmt .dcl]) "

Таким образом, деструктор для исходного объекта A вызывается неявно при выходе из main.

class.dtor¶15 говорит, что «поведение не определено, если деструктор вызывается для объекта, время жизни которого закончилось ([basic.life]).» *

Так что это неопределенное поведение, поскольку исходный A больше не существует (даже если новый a теперь существует в том же хранилище).

Вопрос в том, вызывается ли деструктор для исходного A, или же вызывается деструктор для объекта , в настоящее время называемого a.

Мне известно о basic.life¶7 , в котором говорится, что имя a относится к новому объекту после размещения new. Но class.dtor¶11.3 явно говорит, что вызывается деструктор объекта , выходящий из области действия , а не деструктор объекта , на который ссылается имя, выходящее из области действия .

Я неправильно истолковываю стандарт или это фактически неопределенное поведение?

Редактировать: Несколько человек сказали мне не делать этого. Чтобы уточнить, я определенно не планирую делать это в производственном коде! Это вопрос CppQuiz , который касается угловых случаев, а не лучших практик.

Ответы [ 4 ]

0 голосов
/ 04 сентября 2018

Представьте себе, что вы используете новое место для создания struct B хранилища, в котором живут объекты A a. В конце области действия будет вызван деструктор struct A (поскольку переменная a типа A выходит из области видимости), даже если объект типа B действительно живет там прямо сейчас.

Как уже упоминалось:

"Если программа заканчивает время жизни объекта типа T статическим ([basic.stc.static]), поток ([basic.stc.thread]) или автоматический ([basic.stc.auto]) продолжительность хранения и, если T имеет нетривиальный деструктор, 39 программа должна убедиться, что объект оригинала Тип занимает то же место хранения, когда неявный деструктор звонок происходит ; "

Таким образом, после помещения B в хранилище a вам необходимо уничтожить B и снова поместить туда A, чтобы не нарушать вышеуказанное правило. Это как-то не применимо здесь напрямую, потому что вы помещаете A в A, но это показывает поведение. Это показывает, что это неправильное мышление:

Таким образом, деструктор для исходного объекта A вызывается неявно, когда главные выходы.

Больше нет «оригинального» объекта. На данный момент в хранилище a есть только живой объект. И это все. И на данных, которые в настоящее время находятся в a, вызывается функция, а именно деструктор A. Вот что программа компилирует в. Если бы он волшебным образом отслеживал все «оригинальные» объекты, вы бы каким-то образом имели динамическое поведение во время выполнения.

Дополнительно:

Программа может закончить время жизни любого объекта, повторно используя хранилище который занимает объект или явно вызывая деструктор для объект типа класса с нетривиальным деструктором. Для объекта типа класса с нетривиальным деструктором, программа не требуется явно вызвать деструктор перед хранилищем, которое объект занимает повторно или освобожден; однако, если нет явный вызов деструктора или, если выражение удаления ([expr.delete]) не используется для освобождения хранилища, деструктор не должны вызываться неявно и любая программа, которая зависит от побочные эффекты, создаваемые деструктором, имеют неопределенное поведение.

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

0 голосов
/ 03 сентября 2018

Слишком долго для комментария.

Легкость 'ответ правильный, а его ссылка - правильная ссылка.

Но давайте рассмотрим терминологию более точно. Есть

  • «Срок хранения», относительно памяти.
  • «Время жизни», относительно предметов.
  • «Сфера», относительно имен.

Для автоматических переменных все три совпадают, поэтому мы часто не проводим четкого различия: «переменная выходит из области видимости». То есть: имя выходит за рамки; если это объект с автоматической продолжительностью хранения, вызывается деструктор, заканчивающий время жизни именованного объекта; и, наконец, память освобождается.

Только в вашем примере область имен и продолжительность хранения совпадают & mdash; в любой момент своего существования имя a относится к действительной памяти & mdash; в то время как время жизни объекта разделено между двумя различными объектами в одной и той же ячейке памяти и с одинаковым именем a.

И нет, я думаю, что вы не можете понимать "построенный" в 11.3 как "полностью созданный и не уничтоженный", потому что dtor будет снова вызываться (неправильно), если время жизни объекта преждевременно закончилось предыдущим явный вызов деструктора. Фактически, это одна из проблем, связанных с концепцией повторного использования памяти: если создание нового объекта завершится неудачно с исключением, область действия будет оставлена, и вызов деструктора будет предпринят для незавершенного объекта или для старого объекта, который был удален уже.

Полагаю, вы можете представить себе автоматически выделенную типизированную память, помеченную тегом «быть уничтоженным», который оценивается при разматывании стека. Среда выполнения C ++ на самом деле не отслеживает отдельные объекты или их состояние за пределами этой простой концепции. Поскольку имена переменных в основном являются постоянными адресами, удобно думать, что «имя выходит из области видимости», вызывая вызов деструктора для именованного объекта предполагаемого типа, предположительно присутствующего в этом месте. Если одно из этих предположений неверно, все ставки отменены.

0 голосов
/ 03 сентября 2018

Я неправильно истолковываю стандарт или это фактически неопределенное поведение?

Ни один из них. Стандарт не является неясным, но он может быть более понятным. Однако предполагается, что деструктор нового объекта вызывается, как это подразумевается в [basic.life] p9.

[class.dtor] p12 не очень точный. Я спросил Core об этом, и Майк Миллер (очень старший член) сказал: :

Я бы не сказал, что это противоречие [[class.dtor] p12 против [basic.life] p9], но разъяснение, безусловно, необходимо. Описание деструктора было написано несколько наивно, не принимая во внимание, что исходный объект, занимающий немного места в автоматическом хранилище, мог быть заменен другим объектом, занимающим тот же бит в автоматическом хранилище, но предполагалось, что если конструктор был вызван для этого бит автоматического хранения для создания объекта в нем - то есть, если управление прошло через это объявление - тогда деструктор будет вызываться для объекта, который предположительно занимает этот бит автоматического хранения, когда блок выходит - даже если это не "то же самое" объект, который был создан вызовом конструктора.

Я обновлю этот ответ выпуском CWG, как только он будет опубликован. Итак, ваш код не имеет UB.

0 голосов
/ 03 сентября 2018

Вы неправильно читаете это.

«Деструкторы вызываются неявно для созданных объектов» & hellip; имеется в виду те, которые существуют и их существование дошло до полного строительства. Хотя, возможно, не полностью изложено, оригинал A не соответствует этому критерию, поскольку он больше не «создан»: его вообще не существует! Только новый / замещающий объект автоматически уничтожается, а затем, в конце main, как и следовало ожидать.

В противном случае такая форма размещения new была бы довольно опасной и имела бы дискуссионную ценность в языке. Тем не менее, стоит отметить, что повторное использование фактического A таким образом немного странно и необычно, хотя и не по какой-либо другой причине, кроме как приводящей только к такого рода вопросу. Обычно вы помещаете новый в некоторый пустой буфер (например, char[N] или в некоторое выровненное хранилище), а затем позже сами вызываете деструктор.

Нечто, напоминающее ваш пример, может быть найдено на basic.life & para; 8 & mdash; это UB, но только потому, что кто-то построил T поверх B; формулировка довольно ясно говорит о том, что это единственная проблема с кодом.

Но вот решающий аргумент:

Свойства, приписываемые объектам в настоящем международном стандарте, применяются к данному объекту только в течение срока его службы. [..] [ basic.life & para; 3 ]

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...