Рассмотрим следующую программу на C:
int i = 0;
int post_increment_i() { return i++; }
int main() {
i = post_increment_i();
return i;
}
Что касается версии стандарта C 2011 года (известного как C11), какая из следующих альтернатив верна:
- C11 гарантирует, что main возвращает 0.
- C11 гарантирует, что main возвращает либо 0, либо 1.
- Согласно C11 поведение этой программы не определено.
Соответствующие фрагменты из стандарта C11:
- # P4 #
# P5 # # P6 # # P7 #
- # P8 #
# P9 # # P10 #
- # P11 #
# P12 # # P13 #
- # P14 #
# P15 #
- # P16 #
# P17 #
- # P18 #
# P19 #
Три приведенных выше альтернативы соответствуют следующим трем случаям, соответственно:
- Побочный эффект оператора приращения постфикса упорядочивается перед присваиванием в main.
- Побочный эффект оператора приращения постфикса упорядочивается либо до, либо после присваивания в main, и C11 не указывает, какое именно. (Другими словами, два побочных эффекта имеют неопределенную последовательность.)
- Два побочных эффекта не имеют последовательности.
Похоже, что первая альтернатива верна, исходя из следующей цепочки рассуждений:
Рассмотрим правило Каждая оценка в вызывающей функции (включая вызовы других функций), которая иначе не упорядочена до или после выполнения тела вызываемой функции, имеет неопределенную последовательность относительно выполнения вызываемой функции. < / strong> в 6.5.2.2. Предположение A: побочным эффектом оператора присваивания в основном является такая «оценка». Предположение B: фраза «выполнение вызванной функции» включает в себя как вычисление значения оператора постфиксного приращения, так и побочный эффект оператора постфиксного приращения. Из этих предположений и приведенного выше правила следует, что либо I) вычисление значения, и побочный эффект оператора постфиксного приращения упорядочиваются до побочного эффекта оператора присваивания в основном, либо II) вычисление значения и побочный эффект постфиксного оператора инкремента оба упорядочиваются после побочного эффекта оператора присваивания в main.
Рассмотрим правило Побочный эффект обновления сохраненного значения левого операнда происходит после вычислений значений левого и правого операндов. Это правило исключает случай I, описанный выше. Следовательно, имеет место случай II. QED
В целом это выглядит довольно сильным аргументом. Кроме того, это соответствует тому, что можно было бы интуитивно считать наиболее вероятной альтернативой.
Тем не менее, он полагается на конкретные интерпретации терминов «оценка» и «выполнение вызываемой функции» (предположения A и B) и не совсем простую аргументацию, поэтому я хотел изложить его там, чтобы увидеть, есть ли у людей причины полагать, что это толкование неверно. Обратите внимание, что сноска 94 эквивалентна этой интерпретации, только если она применяется также в том смысле, что вызывающий абонент не чередуется с вызываемым, что, в свою очередь, подразумевает, что «чередование» означает чередование в смысле «abab», поскольку, очевидно, вызывающий абонент чередует с вызываемый в более слабом смысле «аба». Кроме того, альтернативы 2 и 3 кажутся правдоподобными в сценарии, когда компилятор встраивает функцию, а затем выполняет те же виды оптимизации, которые объясняют, почему выражение i = i++
имеет неопределенное поведение.