Почему сам i++ + 1 не является неопределенным? Что гарантирует, что побочный эффект постфикса возникнет после вычисления +?

Я знаю, что этот вопрос часто задают в его версии «i = i++ +1», в которой i появляется дважды, но мой вопрос отличается тем, что он касается ТОЛЬКО правой части этого выражения, определенность которого неочевидна. мне. Я имею в виду только:

i++ + 1;

cppreference.com указывает здесь, что:

2) Вычисления значений (но не побочных эффектов) операндов любого оператора выполняются до вычисления значений результата оператора (но не его побочных эффектов).

Я понимаю, что это означает, что вычисление значения упорядочено, но никаких заявлений о побочном эффекте не делается.

[...]

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

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

Далее в нем говорится:

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

Разве здесь не так? Побочный эффект оператора post-inc на i не является последовательным по сравнению с вычислением значения оператора сложения, который использует тот же i.

Почему это выражение обычно не называют неопределенным?

Это потому, что считается, что оператор сложения влечет за собой вызов функции, для которой даются более строгие гарантии последовательности?


person JMC    schedule 13.11.2019    source источник
comment
Связано, хотя я думаю, что это не дубликат, поскольку вы объяснили, чем он отличается. Также это. Я думаю, вы могли бы рассмотреть эту справочную информацию для будущих читателей.   -  person    schedule 13.11.2019


Ответы (4)


Что гарантирует, что побочный эффект постфикса возникнет после вычисления +?

Нет такой уверенности. Побочный эффект постфикса может возникнуть либо до, либо после вычисления значения + .

Побочный эффект оператора post-inc на i не является последовательным по сравнению с вычислением значения оператора сложения, который использует тот же i.

Нет, вычисление значения оператора сложения использует результат вычисления значения его операндов. Операндами + являются i++ (не i) и 1. Как вы указали в вопросе, чтение i упорядочивается-до вычисления значения i++ и, следовательно, (транзитивность) упорядочено до вычисления значения +.

Следующие вещи гарантированно произойдут в следующем порядке:

  1. Читать i.
  2. Вычисление значения ++ (операнд: результат шага-1)
  3. Вычисление значения + (операнды результат шага 2 и 1)

И побочный эффект i++ должен возникнуть после шага 1, но он может быть где угодно до этого ограничения.

person M.M    schedule 13.11.2019
comment
Нет, вычисление значения оператора сложения использует результат вычисления значения его операндов. Операнды + - это i++ (не i), а 1 О, теперь я понял, спасибо. Я так запутался в формулировках, что больше не понимал, что вычисление значения — это в буквальном смысле процесс определения значения, которое будет использоваться. - person JMC; 13.11.2019

i++ + 1 не является неопределенным из-за использования постфиксного оператора, поскольку он оказывает только один побочный эффект на один объект, и значение этого объекта упоминается только в этом месте. Выражение i++ однозначно создает предыдущее значение i, и это значение добавляется к 1, независимо от того, когда фактически обновляется i.

(Мы не знаем, правильно ли определено i++ + 1, потому что что-то может пойти не так по разным причинам: i неинициализировано или иным образом неопределенно или недействительно, или произошло числовое переполнение или переполнение указателя.)

Неопределенное поведение возникает, если на одной и той же фазе оценки мы пытаемся изменить один и тот же объект дважды: i++ + i++. Это можно запутать с помощью указателей, потому что (*p)++ + (*q)++ увеличивает один и тот же объект только в том случае, если p и q указывают на одно и то же место; в противном случае это нормально.

Неопределенное поведение также возникает, если на той же фазе оценки мы пытаемся наблюдать значение объекта, который изменяется в другом месте выражения, например i++ + i. Правая сторона + обращается к i, но это не упорядочено в отношении побочного эффекта i++ слева; оператор + не навязывает точку последовательности. Излишне говорить, что в i++ + 1 1 не пытается получить доступ к i.

person Kaz    schedule 13.11.2019
comment
Я тоже так всегда об этом думал, пока не вник в конкретную формулировку. Я думаю, что важным моментом является вычисление значения, ИСПОЛЬЗУЮЩЕЕ значение... Теперь я думаю, что это может означать другие вычисления значения самого i, а не вычисления значения, которые включают ранее вычисленное значение i, и в этом случае это не будет применить к оператору +. - person JMC; 13.11.2019
comment
@JMC Формулировка стандарта C менялась с годами, а C ++ имеет свою собственную формулировку. Основные идеи те же, в свете таких простых выражений. - person Kaz; 13.11.2019
comment
@JMC В C90 была формулировка типа ..., используемая только для определения сохраняемого значения. Но затем можно написать выражения, в которых объект используется для определения, где хранится значение. Еще одна проблема заключается в том, что зависимости потока данных имеют логическую последовательность: вы не можете сохранить значение, которое еще не было вычислено, и вы не можете вычислить значение, операнды которого не были доступны. Чисто функциональные языки используют это преимущество для последовательного ввода-вывода (например, монады Haskell и еще много чего). - person Kaz; 13.11.2019
comment
Я знаю, что обычно так это работает и как это описано во второстепенной литературе, мой вопрос скорее в том, были ли именно в стандарте, или что-то близкое к стандарту, заявлено, что это так? При чистом буквальном толковании я не понимаю, почему побочный эффект ++ не мог произойти между оценкой i++ и вычислением сложения. Это потому, что у меня неправильное понимание того, что означает вычисление значения? - person JMC; 13.11.2019
comment
@JMC Оператор + не имеет доступа к значению i; он использует результат выражения i++! Выражение i++ обращается к i, и это разрешено, если это i++ является единственным выражением, делающим это. i++ — это выражение, которое управляет приращением, поэтому, конечно, оно может надежно получить доступ и получить предыдущее значение i. - person Kaz; 13.11.2019
comment
@JMC Что также хорошо определено, так это, скажем, что-то пешеходное, например i = i + i + 1. Это соответствует исходной формулировке C90, согласно которой доступ к i осуществляется только для определения сохраняемого значения. Обновление i с помощью i = требует, чтобы сохраненное значение было вычислено в первую очередь, и оба доступа к i на другой стороне участвуют в создании входных данных, которые идут на вычисление этого значения, поэтому все происходит параллельно. - person Kaz; 13.11.2019
comment
Этот ваш комментарий прояснил для меня, спасибо вам. Я собираюсь принять ответ MM, поскольку это ответ, а не комментарий, и более подробно выражает ту же идею. - person JMC; 13.11.2019
comment
@Kaz: Учитывая struct foo { struct foo *next; } *p1,*p2;, я не думаю, что авторы Стандарта специально намеревались запретить реализации с учетом чего-то вроде p1->next->next = p2; сохранять нижнюю половину p2 в нижнюю половину p1->next->p, а затем сохранять верхнюю половину p2 в верхняя половина p2->next->p (перечитывая p2->next) на платформах, где это был бы наиболее эффективный способ выполнения операции, даже несмотря на то, что это может привести к серьезным сбоям, если p1->next равно p1. С другой стороны, они, по-видимому, ожидали, что такие разделенные записи будут... - person supercat; 13.11.2019
comment
... будет беспокоить только платформы, у которых есть какая-то причина перечитывать p1->next между двумя записями, и что люди, работающие с такими платформами, будут лучше, чем Комитет, оценивать затраты и выгоды от гарантированного поведения в таких случаях. - person supercat; 13.11.2019

Вот что происходит, когда вычисляется i++ + 1:

  • Вычисляется подвыражение i++. Это дает предыдущее значение i.
  • Вычисление i++ также имеет побочный эффект увеличения сохраненного значения i, но обратите внимание, что это увеличенное значение не используется.
  • Вычисление подвыражения 1 дает очевидное значение.
  • Оператор + вычисляется, что дает результат i++ плюс результат 1. Это может произойти только после определения значений левого и правого подвыражений (но это может постоянно происходить до или после возникновения побочного эффекта).

Побочный эффект оператора ++ гарантированно возникает только за некоторое время до следующей точки последовательности. (Это в терминах C99. Стандарт C11 представляет те же правила по-другому.) Но поскольку ничто другое в выражении не зависит от этого побочного эффекта, не имеет значения, когда он возникает. Нет конфликта, поэтому нет неопределенного поведения.

В i++ + i оценка i на RHS будет давать разные результаты в зависимости от того, произошел ли уже побочный эффект или нет. А поскольку порядок не определен, стандарт разводит руками и говорит, что поведение не определено. Но в i++ + i такой проблемы не возникает.

person Keith Thompson    schedule 13.11.2019

«Что гарантирует, что побочный эффект постфикса возникнет после вычисления +?»

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

auto tmp = i;
i = tmp + 1; // Could be done here, or after the next expression, doesn't matter since i isn't read again 
tmp + 1;  // produces actual value of i++ + 1

or

auto tmp = i + 1;
i = tmp; // Could be done here, or after the next expression, doesn't matter since tmp isn't changed again
(tmp - 1) + 1; // produces actual value of i++ + 1

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

++i; // Usually the same as i++ + 1 if compiler has enough knowledge

потому что приращение постфикса, за которым следует добавление единицы, может рассматриваться как приращение префикса без добавления единицы после.

Дело в том, что компилятор должен гарантировать, что побочный эффект возникнет когда-нибудь, что может произойти до или после вычисления +; компилятору просто нужно убедиться, что он сохранил или может восстановить исходное значение i.

Различные искажения здесь могут показаться бессмысленными (очевидно, что ++i лучше всего, если вы можете качать его, а i + 1;, за которым следует ++i, проще всего в противном случае), но они часто необходимы для работы с аппаратными атомами на данной архитектуре; если архитектура предлагает инструкцию fetch_then_add, вы бы хотели реализовать ее как:

 auto tmp = fetch_then_add(i, 1); // Returns original value of i, while atomically adding 1
 tmp + 1;

но если он предлагает только инструкцию add_then_fetch, вам нужно:

auto tmp = add_then_fetch(i, 1); // Returns incremented value of i
(tmp - 1) + 1;

Как и во многих других случаях, стандарт C++ не устанавливает предпочтительный порядок, потому что реальное оборудование не всегда взаимодействует; если он выполняет свою работу и ведет себя так, как задокументировано, на самом деле не имеет значения, какой порядок он использовал.

person ShadowRanger    schedule 13.11.2019