Разница в правилах окончания срока службы?

https://en.cppreference.com/w/cpp/language/lifetime в разделе Примечания имеет этот код, воспроизведенный здесь:

struct A {
  int* p;
  ~A() { std::cout << *p; } // if n outlives a, prints 123
};
void f() {
  A a;
  int n = 123; // if n does not outlive a, this is optimized out (dead store)
  a.p = &n;
}

Что он пытается сказать в этом разделе Примечания?

Насколько я понимаю, это код UB (или он есть), поскольку ясно, что n не переживает a.

Что это значит под:

difference in the end of lifetime rules between non-class objects (end of storage duration) and class objects (reverse order of construction) matters

Но это не говорит о том, что как имеет значение.

Меня очень смущает весь этот раздел.

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

NathanOliver 14.12.2018 20:34

@NathanOliver Я думаю, это может быть намек на то, что n действительно переживает a, потому что a уничтожается непосредственно перед закрывающей фигурной скобкой, а n уничтожается в закрывающей фигурной скобке. Однако я не уверен, что это правда.

Brian Bi 14.12.2018 20:37

Я думал, что разрушение в обратном порядке будет означать, что n уничтожается раньше, чем a приводит к UB.

Galik 14.12.2018 20:54

Это основной вопрос 2256. Мы взяли этот пример (который Clang «неправильно компилирует» в соответствии с текущими правилами) из одного из основных постов отражателя по этой теме.

T.C. 14.12.2018 21:49
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
33
4
1 074
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Это странный аспект правил существования C++. [basic.life] / 1 сообщает нам, что время жизни объекта заканчивается:

  • if Tis a class type with a non-trivial destructor ([class.dtor]), the destructor call starts, or
  • the storage which the object occupies is released, or is reused by an object that is not nested within o ([intro.object]).

Акцент добавлен. int не является «типом класса с нетривиальным деструктором», поэтому его время жизни только заканчивается, когда память, которую он занимает, освобождается. Напротив, A - это тип класса с нетривиальным деструктором ", поэтому его время жизни заканчивается при вызове деструктора.

Хранилище для области действия освобождается при выходе из области в соответствии с [basic.stc.auto] / 1:

The storage for [variables with automatic storage duration] lasts until the block in which they are created exits.

Но автоматические переменные уничтожаются в соответствии с [stmt.jump] / 2:

On exit from a scope (however accomplished), objects with automatic storage duration that have been constructed in that scope are destroyed in the reverse order of their construction.

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

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

Это означает, что код вполне может работать, чтобы n пережил a. Но не уточняется, работает ли делает.

Итак, деструктор для A будет вызываться до выхода из функции f, но я не могу связать его с цитатой, которую вы добавили из стандарта. Небольшая помощь будет оценена.

haccks 14.12.2018 20:51

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

Serge 14.12.2018 20:56

Но разве память для n не будет выпущена до выпуска деструктора / памяти a? Да, я знаю, что память n находится в стеке, но, конечно же, компилятор может использовать ее повторно, прежде чем начинать уничтожение / перераспределение a?

Galik 14.12.2018 20:58

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

Galik 14.12.2018 21:03

@ Галик, нет, в этом вся суть. С точки зрения непрофессионала, время жизни a заканчивается раньше, чем {, а n время жизни заканчивается после {.

SergeyA 14.12.2018 21:03

@SergeyA Насколько мне известно, для уничтожения объекта необходимо вызвать его деструктор (если применимо) и затем освободить его память. Неужто оба случится до перехода к следующему объекту?

Galik 14.12.2018 21:05

@SergeyA: После дальнейшего расследования все не так просто. Мой ответ был соответствующим образом изменен.

Nicol Bolas 14.12.2018 21:07

Как раз тогда, когда мне показалось, что я в основном понимаю правила жизни C++: /

Rakete1111 14.12.2018 21:11

@NicolBolas Мне все еще кажется, что именно предположение компилятор вызовет все деструкторы перед освобождением всей памяти. Я думаю, что как вызов деструкторов и, так и освобождение хранилища происходят на выходе из области видимости - в обратном порядке выделения / построения. Я до сих пор (из этой формулировки) не понимаю, почему каждый объект не будет уничтожен, а затем освобожден по очереди.

Galik 14.12.2018 21:11

@Galik: "Я до сих пор (из этой формулировки) не понимаю, почему каждый объект не будет уничтожен, а затем освобожден по очереди." Я не говорю, что не могло; Я говорю, что это не обязательный. Вот почему страница cppreference гипотетически говорит о том, работает она или нет. Потому что это не указано.

Nicol Bolas 14.12.2018 21:13

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

Brian Bi 14.12.2018 21:22

@ Брайан: Кто такое «мы»? Может комитет? Конечно; по этому поводу было некоторое обсуждение. Может случайные юзеры на SO? Нет.

Nicol Bolas 14.12.2018 21:23

Под «мы» я имел в виду сообщество C++, предлагая комитету сделать такое изменение. Под словом «могу» я имел в виду, будет ли у него приличный шанс быть принятым.

Brian Bi 14.12.2018 21:24

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

SoronelHaetir 14.12.2018 22:03

@SoronelHaetir: На самом деле Брайан просит не об этом. Он просит, чтобы вызов деструктора int закончил его жизнь. Прямо сейчас, даже если вы позвоните n.~int();, это не приведет к окончанию срока службы n.

Nicol Bolas 14.12.2018 22:12

@SoronelHaetir Это покрыто как будто.

T.C. 14.12.2018 22:21

@Brian: Какое преимущество заключается в том, чтобы рассматривать PODS как имеющие время жизни отдельно от хранилища, которое они занимают, по сравнению с разрешением компилятору обрабатывать нагрузки и хранилища, казалось бы, несвязанных lvalue, как обычно неупорядоченных, при указании случаев, когда компиляторы должны распознавать lvalue как связанные или выполнять определенные операции в строгой последовательности? Какая веская причина существует для компилятора забота о времени жизни int?

supercat 14.12.2018 23:22

Этот пример заимствован из Проблема основного языка 2256:

Section: 6.8 [basic.life] Status: drafting Submitter: Richard Smith Date: 2016-03-30

According to 6.4 [basic.lookup] bullet 1.4, the following example has defined behavior because the lifetime of n extends until its storage is released, which is after a's destructor runs:

  void f() { 
    struct A { int *p; ~A() { *p = 0; } } a; 
    int n; 
    a.p = &n; 
  } 

It would be more consistent if the end of the lifetime of all objects, regardless of whether they have a non-trivial destructor, were treated the same.

Notes from the March, 2018 meeting:

CWG agreed with the suggested direction.

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

  1. если время жизни n заканчивается при уничтожении n, программа не определена;

  2. если время жизни n заканчивается до тех пор, пока память не будет освобождена, программа определила поведение 1.

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


1 Это потому, что Проблема основного языка 2115:

Section: 9.6 [stmt.jump] Status: drafting Submitter: Richard Smith Date: 2015-04-16

The relative ordering between destruction of automatic variables on exit from a block and the release of the variables' storage is not specified by the Standard: are all the destructors executed first and then the storage released, or are they interleaved?

Notes from the February, 2016 meeting:

CWG agreed that the storage should persist until all destructions are complete, although the “as-if” rule would allow for unobservable optimizations of this ordering.

Цель состоит в том, чтобы освобождение памяти автоматических переменных происходило после завершения всех разрушений.

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

supercat 14.12.2018 23:19

@supercat: "Есть ли реальная необходимость в том, чтобы у простых объектов старых данных время жизни было отдельным от времени хранения, которое они занимают?" Да. Скалярный анализ времени жизни может выявить людей, использующих неинициализированную память и другие подобные ошибки. Но для этого требуется время жизни скаляров имеют, чтобы вы могли указывать на часть памяти и точно знать (вложенный набор) объектов, которые там живут.

Nicol Bolas 14.12.2018 23:22

Чтобы внести ясность: это означает, что CWG согласилась с тем, что этот фрагмент кода должен имеет неопределенное поведение, и для этого следует обновить стандарт?

Brian Bi 14.12.2018 23:35

@NicolBolas: наличие явных директив, которые либо говорят, что определенная область хранения должна обрабатываться как содержащая неопределенное значение, либо что любые байты в области хранения, которые содержат неопределенное значение, должны быть изменены для хранения неопределенных значений, было бы более полезным для для диагностики и оптимизации, чем пытаться использовать для этой цели правила времени жизни.

supercat 14.12.2018 23:37

@supercat: Но для этого есть «явные директивы». Их называют «уничтожением объекта» и «созданием объекта без его инициализации». Они уже существуют и уже имеют полезные смысловые значения.

Nicol Bolas 15.12.2018 01:24

@NicolBolas: нет возможности сказать: «Я хочу создать объект, инициализированный содержимым базового хранилища, но с неопределенным значением, измененным на неопределенное значение», и я не знаю каких-либо средств уничтожения объекта без каких-либо действий хранилище. Кроме того, представление о том, что время жизни объектов может быть прекращено путем перезаписи хранилища, приводит к раздражающему количеству угловых случаев, которые излишне и бесполезно усложняют или блокируют оптимизацию.

supercat 15.12.2018 01:33

@NicolBolas: Учитывая void test(int *p, double *q, int mode) { *p = 1; *q = 1.0; if (mode) *p = 1;}, что можно сказать о влиянии каждого назначения на время жизни любых затронутых объектов?

supercat 15.12.2018 01:38

@ Брайан: Я не уверен, так как я не член CWG.

xskxzr 15.12.2018 04:31

@ Брайан: Да, это направление.

T.C. 15.12.2018 11:55

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