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

Я пытаюсь выяснить, является ли следующее неопределенным поведением. Я чувствую, что это не 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 называется.

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

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

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

4 ответа

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • "Срок хранения", относительно памяти.
  • "Время жизни", касающееся объектов.
  • "Сфера", касающаяся имен.

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

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

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

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

Представьте себе использование нового размещения для создания 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 вызывается неявно при выходе из main.

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

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

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

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

Другие вопросы по тегам