Порядок оценки и неопределенное поведение

Говоря в контексте стандарта C++11 (который, как вы знаете, больше не имеет понятия точек последовательности), я хочу понять, как определяются два простейших примера.

int i = 0;

i = i++;   // #0

i = ++i;   // #1

В SO есть две темы, которые объясняют эти примеры в контексте C++11. Здесь было сказано, что #0 вызывает UB и #1 четко определен. Здесь было сказано, что оба примера не определены. Эта двусмысленность меня сильно смущает. Я уже трижды читал эту хорошо структурированную ссылку, но эта тема мне кажется слишком сложной.

,

Давайте разберем пример #0: i = i++;,

Соответствующие цитаты:

  • Вычисление значения встроенных операторов postincrement и postdecrement секвенируется перед его побочным эффектом.

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

  • Если побочный эффект на скалярный объект не секвенирован относительно другого побочного эффекта на тот же скалярный объект, поведение не определено.

Насколько я понимаю, побочный эффект оператора присваивания не упорядочен с побочными эффектами его левого и правого аргументов. Таким образом, побочный эффект оператора присваивания не связан с побочными эффектами i++, Так #0 вызывает UB.

,

Давайте разберем пример #1: i = ++i;,

Соответствующие цитаты:

  • Побочный эффект встроенных операторов preincrement и precrementment секвенируется перед вычислением его значения (неявное правило из-за определения в качестве составного присваивания)

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

  • Если побочный эффект на скалярный объект не секвенирован относительно другого побочного эффекта на тот же скалярный объект, поведение не определено.

Я не вижу, чем этот пример отличается от #0, Мне кажется, что это UB по той же причине, что и #0, Побочный эффект назначения не упорядочен с побочным эффектом ++i, Кажется, это UB. Тема, понравившаяся выше, говорит, что она четко определена. Зачем?

,

Вопрос: как я могу применить приведенные правила для определения UB примеров. Было бы очень полезно как можно более простое объяснение. Спасибо!

2 ответа

Решение

Поскольку ваши цитаты не соответствуют стандарту, я постараюсь дать подробный ответ с указанием соответствующих частей стандарта. Определения "побочные эффекты" и "оценка" содержатся в пункте 1.9/12:

Доступ к объекту, обозначенному как volatile glvalue (3.10), изменение объекта, вызов функции библиотечного ввода-вывода или вызов функции, выполняющей любую из этих операций, - все это побочные эффекты, которые являются изменениями в состоянии среды выполнения. Оценка выражения (или подвыражения) в целом включает в себя как вычисления значений (включая определение идентичности объекта для оценки glvalue и выборку значения, ранее назначенного объекту для оценки prvalue), так и инициирование побочных эффектов.

Следующая соответствующая часть - это пункт 1.9/15:

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

Теперь давайте посмотрим, как применить это к двум примерам.

i = i++;

Это постфиксная форма приращения, и вы найдете ее определение в параграфе 5.2.6. Наиболее подходящее предложение гласит:

Вычисление значения выражения ++ выполняется до изменения объекта операнда.

Выражение присваивания см. В параграфе 5.17. В соответствующей части говорится:

Во всех случаях присваивание выполняется после вычисления значения правого и левого операндов и перед вычислением значения выражения присваивания.

Используя всю информацию, приведенную выше, оценка всего выражения выполняется (этот порядок не гарантируется стандартом!):

  • расчет стоимости i++ (Правая сторона)
  • расчет стоимости i (левая сторона)
  • модификация i (побочный эффект ++)
  • модификация i (побочный эффект =)

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

Как насчет второго примера?

i = ++i;

Здесь ситуация совершенно иная. Вы найдете определение приращения префикса в пункте 5.3.2. Соответствующая часть:

Если x не относится к типу bool, выражение ++x эквивалентно x+=1.

Подставляя это, наше выражение эквивалентно

i = (i += 1)

Поиск оператора сложного присваивания += в 5.17/7 мы получаем, что i += 1 эквивалентно i = i + 1 Кроме этого i оценивается только один раз. Следовательно, рассматриваемое выражение, наконец, становится

я = (я = (я + 1))

Но мы уже знаем сверху, что вычисление значения = чередуется после вычисления значения операндов, а побочные эффекты чередуются до вычисления значения =, Таким образом, мы получаем четко определенный порядок оценки:

  1. вычислить значение i + 1 (а также i - левая часть внутреннего выражения)(#1)
  2. инициировать побочный эффект внутреннего =т.е. изменить "внутренний" i
  3. вычислить значение (i = i + 1)что является "новым" значением i
  4. инициировать побочный эффект внешнего =т.е. изменить "внешний" i
  5. вычислить значение полного выражения.

(# 1): Здесь i оценивается только один раз, так как i += 1 эквивалентно i = i + 1 Кроме этого i оценивается только один раз (5.17/7).

Главное отличие в том, что ++i определяется как i += 1, так

i = ++i;

такой же как:

i = (i += 1);

Поскольку побочные эффекты += Оператор секвенируется до вычисления значения оператора, фактической модификации i в ++i последовательность перед внешним назначением. Это следует непосредственно из цитируемых вами разделов: "Побочный эффект (модификация левого аргумента) встроенного оператора присваивания и всех встроенных составных операторов присваивания выполняется после вычисления значения (но не побочных эффектов) как левый, так и правый аргументы, и упорядочивается перед вычислением значения выражения присваивания (то есть перед возвратом ссылки на измененный объект)"

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

И, как вы косвенно указываете, это плохо знакомо с C++11; ранее оба были неопределенными. В более ранних версиях C++ вместо последовательностей раньше использовались точки последовательности, и ни в одном из операторов присваивания не было точки последовательности. (У меня сложилось впечатление, что целью было то, что операторы, которые приводят к lvalue, имеют значение, которое секвенируется после любых побочных эффектов. В более раннем C++ выражение *&++i было неопределенное поведение; в C++ 11 он гарантированно совпадает с++i.)

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