Что происходит с подобъектами скалярного типа после уничтожения объекта?

Рассмотрим этот код (для разных значений renew и cleanse):

struct T {
    int mem;
    T() { }
    ~T() { mem = 42; }
};

// identity functions, 
// but breaks any connexion between input and output
int &cleanse_ref(int &r) {
    int *volatile pv = &r; // could also use cin/cout here
    return *pv;
}

void foo () {
    T t;
    int &ref = t.mem;
    int &ref2 = cleanse ? cleanse_ref(ref) : ref;
    t.~T();
    if (renew)
        new (&t) T;
    assert(ref2 == 42);
    exit(0);
}

assert гарантированно пройдет?

Я понимаю, что этот стиль не рекомендуется. Мнения типа "это неправильная практика" здесь не интересны.

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

РЕДАКТИРОВАТЬ: теперь с двумя вопросами в одном! См. параметр renewrenew == 0 это исходный вопрос).

РЕДАКТИРОВАТЬ 2: Я думаю, мой вопрос действительно таков: что такое объект-член?

РЕДАКТИРОВАТЬ 3: теперь с другим параметром cleanse!


person curiousguy    schedule 24.07.2012    source источник
comment
Минусы быстрые... :(   -  person curiousguy    schedule 24.07.2012
comment
И я не понимаю, почему. Это интересный вопрос. +1 от меня.   -  person Luchian Grigore    schedule 24.07.2012
comment
Если вам не нравится этот вопрос, предлагаю добавить language-lawyer в список игнорируемых тегов.   -  person aschepler    schedule 25.07.2012
comment
Что такое &p? p не объявлено.   -  person Johannes Schaub - litb    schedule 19.08.2012
comment
Стандарт не допускает ваших доказательств. Неисправен в отношении определения времени жизни объекта.   -  person Johannes Schaub - litb    schedule 19.08.2012
comment
@JohannesSchaub-litb П не объявлено. Действительно. Исправлена. Спасибо!   -  person curiousguy    schedule 19.08.2012
comment
Каков рекорд по количеству голосов против технического вопроса?   -  person curiousguy    schedule 06.10.2013
comment
11 минусов. Впечатляющий.   -  person curiousguy    schedule 15.07.2017


Ответы (2)


Сначала у меня были эти две цитаты, но теперь я думаю, что они на самом деле просто указывают, что такие вещи, как int &ref = t.mem;, должны происходить во время жизни t. Что и происходит в вашем примере.

12.7 пункт 1:

Для объекта с нетривиальным деструктором обращение к любому нестатическому члену или базовому классу объекта после завершения выполнения деструктора приводит к неопределенному поведению.

И пункт 3:

Для формирования указателя (или доступа к значению) прямого нестатического члена объекта obj создание obj должно быть начато, а его уничтожение не должно быть завершено, в противном случае вычисление значения указателя (или доступ к члену value) приводит к неопределенному поведению.

Здесь у нас есть полный объект типа T и подобъект-член типа int.

3.8 пункт 1:

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

  • получается хранилище с правильным выравниванием и размером для типа T, и
  • если объект имеет нетривиальную инициализацию, его инициализация завершена.

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

  • если T является типом класса с нетривиальным деструктором (12.4), начинается вызов деструктора или
  • память, которую занимает объект, используется повторно или освобождается.

Кстати, 3.7.3 п1:

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

И 3.7.5:

Продолжительность хранения подобъектов-членов, подобъектов базового класса и элементов массива равна продолжительности их полного объекта (1.8).

Так что не беспокойтесь о том, что компилятор освободит хранилище до exit в этом примере.

В ненормативной заметке в 3.8p2 упоминается, что 12.6.2 описывает время жизни базовых и подобъектов-членов, но язык там говорит только об инициализации и деструкторах, а не о хранении или времени жизни, поэтому я делаю вывод, что этот раздел не влияет на определение времени жизни для подобъектов тривиального типа.

Если я правильно интерпретирую все это, когда renew является ложным, время жизни всего объекта класса заканчивается в конце явного вызова деструктора, НО время жизни подобъекта int продолжается до конца программы.

3.8, параграфы 5 и 6 говорят, что указатели и ссылки на выделенное хранилище до или после времени жизни любого объекта могут использоваться ограниченным образом, и перечисляют множество вещей, которые вы не можете с ними делать. Преобразование Lvalue-to-Rvalue, как и выражение ref == 42, является одной из таких вещей, но это не проблема, если время жизни int еще не закончилось.

Итак, я думаю, что с renew ложью программа правильно сформирована, и assert преуспевает!

Если renew равно true, хранилище повторно используется программой, поэтому время жизни исходного int заканчивается, и начинается время жизни другого int. Но тогда мы попадаем в 3.8 пункт 7:

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

  • хранилище для нового объекта точно перекрывает место хранения, которое занимал исходный объект, и
  • новый объект имеет тот же тип, что и исходный объект (игнорируя cv-квалификаторы верхнего уровня), и
  • тип исходного объекта не является константным, и, если тип класса, не содержит нестатических элементов данных, чей тип является константным или ссылочным типом, и
  • исходный объект был наиболее производным объектом (1.8) типа T, а новый объект является наиболее производным объектом типа T (то есть они не являются подобъектами базового класса).

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

Хотя можно ли по-прежнему использовать ref, в этом примере есть еще одна проблема.

12.6.2 пункт 8:

Если после завершения вызова конструктора для класса X член класса X не инициализируется и ему не присваивается значение во время выполнения составного оператора тела конструктора, член имеет неопределенное значение. .

Это означает, что реализация совместима, если она устанавливает t.mem в ноль или 0xDEADBEEF (и иногда режимы отладки действительно делают такие вещи перед вызовом конструктора).

person aschepler    schedule 24.07.2012
comment
Итак, объекты в C++ могут иметь три состояния: построено (ctor завершен), построено (ctor вызван, не завершен), не построен (ctor не вызван)? Где это объясняется? - person curiousguy; 24.07.2012
comment
Относительно цитаты из 12.6.2 п. 8, то есть представляется ненормативной заметкой, и я не уверен, что она верна во всех случаях. В противном случае нормативный язык выглядел бы так: объект инициализируется по умолчанию ... Если рассматриваемый объект имеет статическую продолжительность хранения и, следовательно, ранее был инициализирован нулем, оставит ли последующая инициализация по умолчанию объект равным нулю или он получает неопределенное значение? - person user1998586; 31.10.2015
comment
@aschepler Если у вас есть интерес, я задал вопрос, надеясь лучше понять текст 12.6.2: stackoverflow.com/questions/33456141/. - person user1998586; 31.10.2015
comment
Итак, я думаю, что с renew false программа правильно сформирована и утверждение выполнено успешно!, но это противоречит 12.7p3, которое вы цитируете. Явный вызов деструктора уничтожил объект, поэтому, если вы впоследствии читаете ссылку, это UB (потому что вы пытаетесь получить доступ к значению нестатического члена разрушенного объекта). - person Johannes Schaub - litb; 01.11.2015

Вы не уничтожили память, вы только вручную вызвали деструктор (в этом контексте это не отличается от вызова обычного метода). Память (часть стека) вашей переменной t не была "освобождена". Таким образом, это утверждение всегда будет проходить с вашим текущим кодом.

person tumdum    schedule 24.07.2012
comment
Но рассмотрим assert(t.mem == 42): он будет использовать несуществующий объект, если renew равно 0. - person curiousguy; 24.07.2012