Порядок оценки и неопределенное поведение
Говоря в контексте стандарта 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))
Но мы уже знаем сверху, что вычисление значения =
чередуется после вычисления значения операндов, а побочные эффекты чередуются до вычисления значения =
, Таким образом, мы получаем четко определенный порядок оценки:
- вычислить значение
i + 1
(а такжеi
- левая часть внутреннего выражения)(#1) - инициировать побочный эффект внутреннего
=
т.е. изменить "внутренний"i
- вычислить значение
(i = i + 1)
что является "новым" значениемi
- инициировать побочный эффект внешнего
=
т.е. изменить "внешний"i
- вычислить значение полного выражения.
(# 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
.)