В С++ 11 поведение `i + = ++ я + 1` демонстрирует поведение undefined?

Этот вопрос возник во время чтения (ответы) Итак, почему я = ++ я + 1 четко определен в С++ 11?

Я понимаю, что тонкое объяснение состоит в том, что (1) выражение ++i возвращает lvalue, но + принимает prvalues ​​как операнды, поэтому необходимо выполнить преобразование от lvalue до prvalue; это включает в себя получение текущего значения этого lvalue (а не более старого значения i) и поэтому должно быть секвенировано после побочного эффекта от приращения (т.е. обновления i) (2) LHS присвоение также является значением lvalue, поэтому его оценка стоимости не предполагает выборку текущего значения i; в то время как вычисление этого значения не зависит от w.r.t. вычисление значения RHS, это не представляет проблемы (3) вычисление значения самого присваивания включает в себя обновление i (снова), но последовательность после вычисления значения его RHS и, следовательно, после предварительного обновления до i; нет проблем.

Хорошо, поэтому там нет UB. Теперь мой вопрос заключается в том, что если изменить оператор-ассистент от = до += (или аналогичный оператор).

Выполняет ли оценка выражения i += ++i + 1 поведение undefined?

Как я вижу, стандарт, кажется, противоречит здесь. Поскольку LHS += все еще является lvalue (и его RHS все еще является prvalue), те же рассуждения, что и выше, применяются в отношении (1) и (2); в evalutation операндов на += не существует поведения undefined. Что касается (3), то операция составного присваивания += (точнее, побочный эффект этой операции, вычисление ее значения, если необходимо, в любом случае, секвенированный после ее побочного эффекта) теперь должны как извлекать текущее значение i, а затем (очевидно, после него, даже если стандарт не говорит об этом явно, или иначе оценка таких операторов всегда вызывает поведение undefined), добавьте RHS и верните результат обратно в i. Обе эти операции дали бы поведение undefined, если бы они были не подвержены последовательности w.r.t. побочный эффект ++, но, как указано выше (побочный эффект ++ секвенирован перед вычислением значения +, дающий RHS оператора +=, вычисление значения которого секвенировано до операции этого составного назначения), это не так.

Но, с другой стороны, стандарт также говорит, что E += F эквивалентен E = E + F, за исключением того, что (lvalue) E оценивается только один раз. Теперь в нашем примере вычисляется значение i (что и есть E здесь), поскольку lvalue не содержит ничего, что должно быть секвентировано w.r.t. другие действия, поэтому делать это один или два раза не имеет значения; наше выражение должно быть строго эквивалентно E = E + F. Но вот проблема; довольно очевидно, что оценка i = i + (++i + 1) даст поведение undefined! Что дает? Или это дефект стандарта?

Добавлено.. Я немного изменил свое обсуждение выше, чтобы больше оправдать правильное различие между побочными эффектами и вычислениями значений и использовать "оценку" (как и стандарт) выражения для охватывают оба. Я думаю, что мой основной допрос касается не только того, определено ли поведение в этом примере, но и как нужно прочитать стандарт, чтобы решить это. Примечательно, что если принять эквивалентность от E op= F до E = E op F как окончательного авторитета для семантики сложной операции присваивания (в этом случае пример явно имеет UB) или просто как указание на то, что математическая операция задействована в определяя значение, которое нужно назначить (а именно тот, который идентифицирован op, с преобразованным Lvalue-to-rLL LHS оператора присваивания назначений в качестве левого операнда и его RHS как правый операнд). Последний вариант затрудняет обсуждение UB в этом примере, как я пытался объяснить. Я признаю, что соблазнительно сделать эквивалентность авторитетной (так что составные назначения становятся своего рода примитивами второго класса, смысл которых задается переписанием в терминах первоклассных примитивов, поэтому упрощение определения языка), но там являются довольно сильными аргументами против этого:

  • Эквивалентность не является абсолютной, поскольку исключение "E оценивается только один раз". Обратите внимание, что это исключение необходимо для того, чтобы избежать использования, когда оценка E включает поведение побочного эффекта undefined, например, в довольно распространенном использовании a[i++] += b;. Если, по-моему, нет абсолютно эквивалентного переписывания для устранения сложных назначений; используя фиктивный оператор ||| для обозначения неследовательных оценок, можно попытаться определить E op= F;int операндами для простоты) как эквивалент { int& L=E ||| int R=F; L = L + R; }, но тогда в примере больше нет UB. В любом случае стандарт не дает нам рецепта recwriitng.

  • Стандарт не обрабатывает составные назначения как примитивы второго класса, для которых не требуется отдельное определение семантики. Например, в 5.17 (акцент мой)

    Оператор присваивания (=) и составные операторы присваивания все группы справа налево. [...] Во всех случаях присвоение упорядочивается после значения вычисление правого и левого операндов и перед вычислением значения выражения присваивания. В отношении неопределенного вызова последовательности функций операция составного присвоения представляет собой единую оценку.

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

Если кто-то признает, что составные присвоения имеют собственную семантику, то возникает точка, в которой их оценка включает (помимо математической операции) больше, чем просто побочный эффект (присвоение) и оценку стоимости (упорядочивается после присвоения), но также и неназванная операция получения (предыдущего) значения LHS. Обычно это рассматривается под заголовком "преобразование lvalue-to-rvalue", но сделать это здесь трудно оправдать, поскольку нет оператора, который принимает LHS как операнд rvalue (хотя есть один в расширенном "эквивалентная" форма). Именно эта неназванная операция, потенциальная невязкая связь с побочным эффектом ++ вызовет UB, но это неэфферентное отношение нигде не указано явно в стандарте, потому что безымянная операция не является. Трудно обосновать UB, используя операцию, само существование которой только неявно в стандарте.

Ответ 1

Нет четкого примера для Undefined Поведение здесь

Конечно, аргумент, приводящий к UB, может быть задан, как я указал в вопросе, и который был повторен в ответах, данных до сих пор. Однако это связано с строгим чтением 5.17: 7, которое противоречиво и противоречиво с явными утверждениями в 5.17: 1 о сложном назначении. При более слабом чтении 5.17: 7 противоречия исчезают, как и аргумент для UB. Отсюда мой вывод заключается не в том, что здесь есть UB, а в том, что существует четко определенное поведение, но текст стандарта несовместим и должен быть изменен, чтобы четко определить, какое чтение преобладает (и я полагаю это означает, что отчет о дефектах должен быть написан). Конечно, здесь можно было бы ссылаться на предложение fall-back в стандарте (примечание в 1.3.24), согласно которому оценки, для которых стандарт не может определить поведение [однозначно и самосогласованно], являются Undefined Behavior, но это будет делать любое использование составных присвоений (включая префиксы приращения/сокращения операторов) в UB, что может понравиться некоторым разработчикам, но, конечно, не для программистов.

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

int& f (int& a) { return a; }

функция, которая ничего не делает и возвращает свой аргумент (lvalue). Теперь измените пример на

n += f(++n) + 1;

Обратите внимание, что, хотя в стандарте даны некоторые дополнительные условия о последовательности вызовов функций, на первый взгляд это, похоже, не влияет на пример, так как никакого побочного эффекта от вызова функции (даже не локально внутри функция), поскольку приращение происходит в выражении аргумента для f, оценка которого не подлежит этим дополнительным условиям. Действительно, применим Критический аргумент для Undefined Behavior (CAUB), а именно 5.17: 7, в котором говорится, что поведение такого составного присвоения эквивалентно поведению (в данном случае)

n = n + f(++n) + 1;

за исключением того, что n оценивается только один раз (исключение здесь не имеет значения). Оценка выражения, которое я только что написал , явно имеет UB (вычисление значения первого (prvalue) n в RHS не зависит от побочного эффекта операции ++, которая включает в себя тот же скалярный объект (1.9: 15), и вы мертвы).

Итак, оценка n += f(++n) + 1 имеет поведение Undefined, правильно? Неверно! Прочитайте в 5.17: 1, что

Что касается вызова функции с неопределенной последовательностью, операция составного присваивания представляет собой единую оценку. [Примечание: поэтому вызов функции не должен вмешиваться между преобразованием lvalue-to-rval и побочным эффектом, связанным с любым единственным оператором присваивания. - конечная нота]

Этот язык далек от столь же точного, насколько мне бы хотелось, но я не думаю, что это растягивание, чтобы предположить, что "неопределенно-секвенированный" должен означать "в отношении этой операции составного назначения". Замечание (не нормативное, я знаю) дает понять, что преобразование lvalue-to-rval является частью операции составного присвоения. Теперь вызов f неопределенно-секвентирован относительно операции составного присвоения +=? Я не уверен, потому что отношение "sequenced" определено для вычислений отдельных значений и побочных эффектов, а не для полных оценок операторов, которые могут включать оба. Фактически оценка сложного оператора присваивания включает в себя три элемента: преобразование lvalue-to-rale его левого операнда, побочный эффект (собственно присвоение) и вычисление значения составного назначения (которое секвенируется после побочного эффекта, и возвращает исходный левый операнд как lvalue). Обратите внимание, что существование преобразования lvalue-rvalue никогда не упоминается в стандарте, за исключением упомянутой выше заметки; в частности, стандарт вообще не делает никаких других заявлений относительно его последовательности относительно других оценок. Довольно ясно, что в примере вызов f секвенирован перед вычислением побочного эффекта и значения += (поскольку вызов происходит при вычислении значения правого операнда до +=), но он может быть неопределенно-упорядоченной относительно части преобразования lvalue-to-rvalue. Я помню из своего вопроса, что, поскольку левый операнд += является lvalue (и обязательно так), невозможно преобразовать преобразование lvalue-to-rval, которое произошло как часть вычисления значения левого операнда.

Однако по принципу исключенной середины вызов f должен быть либо неопределенно-секвентированным относительно операции составного присвоения +=, либо не определенно-секвентированный по отношению к нему; в последнем случае он должен быть секвентирован перед ним, потому что после него он не может быть секвенирован (вызов f секвенирован перед побочным эффектом +=, а отношение антисимметричное). Поэтому сначала предположим, что он неопределенно-секвентирован относительно операции. Затем в цитированном пункте говорится, что w.r.t. вызов f оценка += - это одна операция, и примечание объясняет, что это означает, что вызов не должен вмешиваться между преобразованием lvalue-to-rval и побочным эффектом, связанным с +=; он должен быть либо секвентирован перед обоими, либо после обоих. Но после секвенсирования побочный эффект невозможен, поэтому он должен быть до обоих. Это делает (по транзитивности) побочный эффект ++ секвенированным до преобразования lvalue-to-r, выход UB. Затем предположим, что вызов f секвенирован до операции +=. Затем он, в частности, секвенируется перед преобразованием lvalue-to-rvalue, и снова транзитивностью, так что это побочный эффект ++; нет UB в этой ветке.

Заключение: 5.17: 1 противоречит 5.17: 7, если последнее взято (CAUB) как нормативное для вопросов UB, полученное в результате неосуществимых оценок на 1.9: 15. Как я уже сказал, CAUB тоже противоречиво (аргументами, указанными в вопросе), но этот ответ подходит к концу, поэтому я оставлю его на этом пока.

Три проблемы и два предложения для их решения

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

  • Текст 5.17: 7 имеет очевидную простоту в том, что, хотя намерение легко понять, мы мало удержаемся при применении к сложным ситуациям. Он делает радикальное утверждение (эквивалентное поведение, по-видимому, во всех аспектах), но чье применение пресечено предложением исключения. Что, если поведение E1 = E1 op E2 undefined? Ну, тогда должно быть и то E1 op = E2. Но что, если UB был из-за E1 оценивается дважды в E1 = E1 op E2? Тогда оценка E1 op = E2 должна, по-видимому, не быть UB, но если да, то определяется как что? Это похоже на то, что "молодежь второго близнеца была похожа на молодость, за исключением того, что он не умер при родах". Честно говоря, я думаю, что этот текст, который мало развился после версии C "Совокупное присвоение формы E1 op = E2 отличается от простого выражения присваивания E1 = E1 op E2 только тем, что lvalue E1 оценивается только один раз." могут быть адаптированы для соответствия изменениям стандарта.

    (5.17) 7 Поведение выражения вида E1 op = E2 эквивалентно E1 = E1 op E2, за исключением того, что E1 оценивается только один раз. [...]

  • Не совсем ясно, какие именно действия (оценки) находятся между которыми определяется "упорядоченное" отношение. Говорят (1.9: 12), что оценка выражения включает вычисления значений и инициирование побочных эффектов. Хотя это, по-видимому, говорит о том, что оценка может иметь несколько (атомных) компонентов, секвенциальное отношение фактически определяется главным образом (например, в 1.9: 14,15) для отдельных компонентов, так что было бы лучше читать это как то, что понятие "оценка" охватывает как вычисления значений, так и (инициирование) побочных эффектов. Однако в некоторых случаях отношение "упорядочено" определено для (полного) выполнения выражения выражения (1.9: 15) или для вызова функции (5.17: 1), хотя переход в 1.9: 15 исключает его ссылаясь непосредственно на выполнение в теле вызываемой функции.

    (1.9) 12 Оценка выражения (или подвыражения) в целом включает как вычисления значений (...), так и инициирование побочных эффектов. [...] 13 Последовательность - это асимметричное, транзитивное, парное отношение между оценками, выполненными одним потоком [...] 14 Каждое вычисление значения и побочный эффект, связанный с полным выражением, секвенируются перед каждым вычислением значения и побочный эффект, связанный со следующим полным выражением, которое должно быть оценено. [...] 15 При вызове функции (независимо от того, является ли функция встроенной), каждое вычисление значения и побочный эффект, связанное с любым выражением аргумента, или с выражением postfix, обозначающим вызываемую функцию, является секвенированы перед выполнением каждого выражения или выражения в теле вызываемой функции. [...] Каждая оценка в вызывающей функции (включая другие вызовы функций)... неопределенно упорядочена относительно выполнения вызываемой функции [...] (5.2.6, 5.17) 1... Что касается вызова с неопределенной последовательностью функций,...

  • Текст должен более четко признать, что составное присвоение включает, в отличие от простого назначения, действие извлечения значения, ранее назначенного его левому операнду; это действие похоже на преобразование lvalue-to-rvalue, но не выполняется как часть вычисления значения этого левого операнда, поскольку это не значение prvalue; действительно, проблема заключается в том, что 1,9: 12 только признает такие действия для оценки стоимости. В частности, текст должен быть более ясным, о каких "секвентированных" отношениях даны для этого действия, если они есть.

    (1.9) 12 Оценка выражения... включает в себя... вычисления значений (включая определение идентичности объекта для оценки glvalue и получение значения, ранее назначенного объекту для оценки оценки)

Второй момент - это наименее непосредственное отношение к нашему конкретному вопросу, и я думаю, что его можно решить просто, выбирая четкую точку зрения и переформулируя пазы, которые, как представляется, указывают на другую точку зрения. Учитывая, что одна из основных целей старых точек последовательности, а теперь и "упорядоченное" отношение, заключалась в том, чтобы ясно показать, что побочный эффект операторов postfix-increment не зависит от w.r.t. к действиям, последовательно упорядоченным после вычисления значения этого оператора (таким образом, например, i = i++ UB), точка зрения должна заключаться в том, что вычисления отдельных значений и (инициирование) отдельных побочных эффектов являются "оценками", для которых "секвенирование до" может быть определены. По прагматичным причинам я бы также включил еще два вида (тривиальных) "оценок": запись функции (так что язык 1.9: 15 можно упростить так: "При вызове функции... каждое вычисление значения и связанный с ним побочный эффект с любым из выражений его аргументов или с выражением postfix, обозначающим вызываемую функцию, секвентируется перед входом этой функции" ) и выхода функции (так что любое действие в теле функции получает с помощью последовательности транзитивности перед тем, что требует значения функции; это было гарантировано точкой последовательности, но стандарт С++ 11, похоже, потерял такую ​​гарантию, что может вызвать вызов функции, заканчивающейся с return i++; потенциально UB, где это не предназначено и используется для обеспечения безопасности). Тогда также можно четко указать "неопределенно упорядоченное" отношение вызовов функций: для каждого вызова функции и для каждой оценки, которая не является (прямо или косвенно) частью оценки этого вызова, эта оценка должна быть секвенирована (до или после) WRT как вход, так и выход этого вызова функции, и он должен иметь одинаковую связь в обоих случаях (так что, в частности, такие внешние действия не могут быть секвенированы после ввода функции, но до выхода функции, что явно желательно в пределах одного потока).

Теперь, чтобы решить точки 1 и 3., я вижу два пути (каждый из которых влияет на обе точки), которые имеют разные последствия для определенного или не поведения нашего примера:

Смежные присваивания с двумя операндами и тремя оценками

Сложные операции имеют два обычных операнда, левый операнд lvalue и правый операнд prvalue. Чтобы установить нечеткость 3, он включен в 1.9: 12, что выборка, ранее назначенная объекту, также может возникать в составных присвоениях (а не только для оценки стоимости). Семантика заданных заданий определяется изменением 5.17: 7 на

В составном присваивании op = выбирается значение, ранее назначенное объекту, на которое ссылается левый операнд, оператор op применяется с этим значением как левый операнд, а правый операнд op = как правый операнд, и результирующее значение заменяет объект, на который ссылается левый операнд.

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

Для ясности ясно укажем в 1.9: 15, что вычисления значений в операндах упорядочены до всех вычислений значений, связанных с оператором (а не только для результата оператора), что гарантирует, что оценка левого операнда lvalue будет секвенирована перед тем, как извлечь его значение (вряд ли можно представить себе другое), а также последовательно вычисляет значение правильного операнда до этого выборки, исключая, таким образом, UB в нашем примере. Хотя на нем я не вижу причин не проводить вычисления последовательностей в операндах перед любыми побочными эффектами, связанными с оператором (как они явно должны); это будет упоминать это явно для (составных) присвоений в 5.17: 1 излишним. С другой стороны, отметим, что значение, получаемое в составном назначении, секвенируется перед его побочным эффектом.

Составные назначения с тремя операндами и двумя оценками

Чтобы получить, что выборка в присваивании задания будет несущественным по отношению к вычислению значения правого операнда, делая наш пример UB, самым ясным способом является предоставление сложным операторам неявного третьего (среднего) операнда, prvalue, не представленное отдельным выражением, но полученное преобразованием lvalue-rvalue из левого операнда (этот трехоперационный характер соответствует расширенной форме составных назначений, но, получив средний операнд из левого операнда, обеспечивается, чтобы значение извлекалось из того же объекта, к которому будет храниться результат, - важная гарантия, которая только неопределенно и неявно указана в текущей формулировке посредством "за исключением того, что E1 оценивается только один раз" ). Разница с предыдущим решением заключается в том, что выборка теперь является подлинным преобразованием lvalue-to-rvalue (поскольку средний операнд является prvalue) и выполняется как часть вычисления значения операндов для составного присвоения, что делает его естественным нечувствительный к вычислению значения правого операнда. Следует указать где-нибудь (в новом разделе, описывающем этот неявный операнд), что вычисление значения левого операнда секвенируется перед этим преобразованием lvalue-to-rvalue (это явно необходимо). Теперь 1.9: 12 можно оставить как есть, а вместо 5.17: 7 предлагаю

В составном присваивании op = с левым операндом a (lvalue) и средними и правыми операндами b соответственно c (оба prvalues) оператор op применяется с b как слева операнд и c как правый операнд, а результирующее значение заменяет объект объекта, на который указывает a.

(Это дает одну оценку, побочный эффект, со второй оценкой вычисления тривиального значения составного оператора, секвенированного после него.)

Все еще применимые изменения в вариантах 1.9: 15 и 5.17: 1, предложенные в предыдущем решении, могут по-прежнему применяться, но не дают нашего исходного примера определенного поведения. Однако модифицированный пример в верхней части этого ответа по-прежнему будет определять поведение, если только часть 5.17: 1 "составное присвоение является одной операцией" не будет отменена или не будет изменена (имеется аналогичный переход в 5.2.6 для приращения/уменьшения постфикса), Существование этих отрывков предполагает, что отключение операций fecth и хранения в рамках одного составного присвоения или приращения/декремента постфикса не было намерением тех, кто написал текущий стандарт (и по расширению, делающий наш пример UB), но это, конечно, просто догадки.

Ответ 2

Об описании i = ++i + 1

Я понимаю, что тонкое объяснение состоит в том, что

(1) выражение ++i возвращает lvalue, но + принимает prvalues ​​как операнды, поэтому необходимо выполнить преобразование от lvalue до prvalue;

Вероятно, см. Активная проблема CWG 1642.

это предполагает получение текущее значение этого lvalue (а не больше, чем старое значение of i) и поэтому должны быть секвенированы после побочного эффекта от инкремент (т.е. обновление i)

Здесь определяется последовательность для приращения (косвенно, через +=, см. (a)): Побочный эффект ++ (модификация i) секвенирован перед вычислением значения всего выражения ++i. Последнее относится к вычислению результата ++i, а не к загрузке значения i.

(2) LHS присвоения также является lvalue, поэтому его оценка стоимости не предполагает выборку текущего значение i; в то время как вычисление этого значения не зависит от w.r.t. вычисление значения RHS, это не представляет проблемы

Я не думаю, что это правильно определено в Стандарте, но я бы согласился.

(3) значение вычисление самого присваивания включает в себя обновление i (снова),

Вычисление значения i = expr требуется только при использовании результата, например. int x = (i = expr); или (i = expr) = 42;. Само вычисление значения не изменяет i.

Модификация i в выражении i = expr, которая происходит из-за =, называется побочным эффектом =. Этот побочный эффект секвенирован до вычисления значения i = expr - или, скорее, вычисление значения i = expr секвенируется после побочного эффекта присваивания в i = expr.

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

но секвенируется после вычисления значения его RHS и, следовательно, после предыдущее обновление до i; нет проблем.

Побочный эффект присваивания i = expr секвенируется после вычисления значения операндов i (A) и expr присвоения.

expr в этом случае является выражением +: expr1 + 1. Вычисление значения этого выражения секвенируется после вычисления значений его операндов expr1 и 1.

expr1 здесь ++i. Вычисление значения ++i секвенируется после побочного эффекта ++i (модификация i) (B)

Вот почему i = ++i + 1 является безопасным. Там цепочка секвенирована раньше между вычислением значения в (A) и побочным эффектом на ту же переменную в (B).


(a) Стандарт определяет ++expr в терминах expr += 1, который определяется как expr = expr + 1, когда expr оценивается только один раз.

Для этого expr = expr + 1, следовательно, мы имеем только одно вычисление значения expr. Побочный эффект = секвенирован перед вычислением значения целого expr = expr + 1, и он упорядочивается после вычисления значения операндов expr (LHS) и expr + 1 (RHS).

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


О i += ++i + 1

Сопоставляет ли вычисление значения i += ++i + 1 поведение undefined?

Поскольку LHS += все еще является lvalue (и его RHS все еще является prvalue), то же самое рассуждения, как указано выше, применяются в отношении (1) и (2); что касается (3) вычисление значения оператора += теперь должно как извлечь текущее значение i, а затем (очевидно, секвентированное после него, даже если стандарт не говорит об этом явно, или иначе выполнение такие операторы всегда будут вызывать поведение undefined). добавление RHS и сохранение результата обратно в i.

Я думаю здесь проблема: добавление i в LHS i += к результату ++i + 1 требует знания значения i - вычисления значения (что может означать загрузку значения i). Это вычисление значения не зависит от модификации, выполняемой ++i. Это, по сути, то, что вы говорите в своем альтернативном описании, следуя переписанию, указанному в стандарте i += expri = i + expr. Здесь вычисление значения i внутри i + expr не влияет на вычисление значения expr. Что вы получите UB.

Обратите внимание, что вычисление значения может иметь два результата: "адрес" объекта или значение объекта. В выражении i = 42 вычисление значения lhs "создает адрес" i; то есть компилятор должен выяснить, где хранить rhs (по правилам наблюдаемого поведения абстрактной машины). В выражении i + 42 вычисляется значение i. В предыдущем абзаце я имел в виду второй тип, поэтому применяется [intro.execution] p15:

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


Другой подход для i += ++i + 1

вычисление значения оператора += теперь должно как извлечь текущее значение i, , а затем [...] выполнить добавление RHS

Значение RHS ++i + 1. Вычисление результата этого выражения (вычисление значения) не зависит от вычисления значения i из LHS. Поэтому слово в этом предложении вводит в заблуждение: конечно, он должен сначала загрузить i, а затем добавить к нему результат RHS. Но нет никакого порядка между побочным эффектом RHS и вычислением значения, чтобы получить значение LHS. Например, вы можете получить для LHS либо старое, либо новое значение i, измененное RHS.

В общем случае хранилище и "параллельная" загрузка - это гонка данных, что приводит к undefined Поведение.


Адресация добавления

с помощью фиктивного оператора ||| для обозначения неследовательных оценок можно попытаться определить E op= F; (с int операндами для простоты) как эквивалент { int& L=E ||| int R=F; L = L + R; }, но тогда в примере больше нет UB.

Пусть E be i и F be ++i (нам не нужен + 1). Тогда для i = ++i

int* lhs_address;
int lhs_value;
int* rhs_address;
int rhs_value;

    (         lhs_address = &i)
||| (i = i+1, rhs_address = &i, rhs_value = *rhs_address);

*lhs_address = rhs_value;

С другой стороны, для i += ++i

    (         lhs_address = &i, lhs_value = *lhs_address)
||| (i = i+1, rhs_address = &i, rhs_value = *rhs_address);

int total_value = lhs_value + rhs_value;
*lhs_address = total_value;

Это предназначено для представления моего понимания гарантий последовательности. Обратите внимание, что оператор , выполняет все вычисления и побочные эффекты LHS перед значениями RHS. Скобки не влияют на последовательность. Во втором случае, i += ++i, мы имеем модификацию i unsequenced от преобразования lvalue-to-rval i = > UB.

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

Я бы сказал, что избыточность. Переписывание от E1 op = E2 до E1 = E1 op E2 также включает в себя, какие типы выражений и категории значений требуются (на rhs, 5.17/1 говорит что-то о lhs), что происходит с типами указателей, требуемыми преобразованиями и т.д. Печально то, что что предложение о "В отношении..." в 5.17/1 не относится к 5.17/7 в качестве исключения этой эквивалентности.

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

Как только мы помещаем это "по отношению к.." также в список исключений из 5.17/7, я не думаю, что есть противоречие.

Как выясняется, как вы можете видеть в ответе Марка ван Леувена, это предложение приводит к следующему интересному наблюдению:

int i; // global
int& f() { return ++i; }
int main() {
    i  = i + f(); // (A)
    i +=     f(); // (B)
}

Кажется, что (A) имеет два возможных результата, так как вычисление тела F неопределенно секвенировано с вычислением значения i в i + f().

В (B), с другой стороны, оценка тела f() секвенируется перед вычислением значения i, так как += следует рассматривать как одну операцию, а f() конечно необходимо оценить до назначения +=.

Ответ 3

Выражение:

i += ++i + 1

вызывает поведение undefined. Метод адвоката языка требует от нас возврата к отчету о дефекте, который приводит к:

i = ++i + 1 ;

становится хорошо определенным в С++ 11, который является отчет о дефекте 637. Правила и пример последовательности не согласны, начинается:

В пункте 1.9 [intro.execution] 16, следующее выражение все еще перечислены в качестве примера поведения undefined:

i = ++i + 1;

Однако, похоже, что новые правила секвенирования делают это выражение четко определенные

Логика, используемая в отчете, выглядит следующим образом:

  • После вычисления значений как его LHS, так и RHS (5.17 [expr.ass], пункт 1) необходимо выполнить следующий побочный эффект.

  • LHS (i) является lvalue, поэтому его вычисление значения включает вычисление адреса i.

  • Чтобы вычислить значение RHS (++ я + 1), необходимо сначала вычислить выражение lvalue ++ i, а затем выполнить преобразование lvalue-to-rval в результате. Это гарантирует, что побочный эффект приращения секвенирован перед вычислением операции сложения, которая, в свою очередь, секвенируется перед побочным эффектом присваивания. Другими словами, он дает четко определенный порядок и конечное значение для этого выражения.

Таким образом, в этом вопросе наша проблема изменяет RHS, которая идет от:

++i + 1

в

i + ++i + 1

из-за черновик проекта С++ 11 5.17 Операторы присваивания и составного присваивания, которые гласят:

Поведение выражения вида E1 op = E2 эквивалентно E1 = E1 op E2, за исключением того, что E1 оценивается только один раз. [...]

Итак, теперь мы имеем ситуацию, когда вычисление i в RHS не секвенируется относительно ++i, и поэтому мы имеем поведение undefined. Это следует из параграфа 1.9, который гласит:

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

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

warning: unsequenced modification and access to 'i' [-Wunsequenced]
i += ++i + 1 ;
  ~~ ^

для этого кода:

int main()
{
    int i = 0 ;

    i += ++i + 1 ;
}

Это подкрепляется этим явным тестовым примером в clang's наборе тестов для - Wunsequenced:

 a += ++a; 

Ответ 4

Да, это UB!

Оценка вашего выражения

i += ++i + 1

выполняется в следующих шагах:

5.17p1 (С++ 11) указывает (подчеркивает мое):

Оператор присваивания (=) и составные операторы присваивания все группы справа налево. Все они требуют модифицируемого lvalue в качестве своего левого операнда и возвращают lvalue, ссылаясь на левый операнд. Результатом во всех случаях является бит-поле, если левым операндом является бит-поле. Во всех случаях назначение выполняется после вычисления значения правого и левого операндов и перед вычислением значения выражения присваивания.

Что означает "вычисление значения"?

1.9p12 дает ответ:

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

Поскольку ваш код использует сложный оператор присваивания, 5.17p7 сообщает нам, как ведет себя этот оператор:

Поведение выражения формы E1 op= E2 эквивалентно E1 = E1 op E2 except that E1 оценивается только один раз.

Следовательно, оценка выражения E1 ( == i) включает в себя как определение идентификатора объекта, обозначенного i, так и преобразование lvalue-to-rvalue для извлечения значения, хранящегося в этом объекте. Но оценка двух операндов E1 и E2 не упорядочена относительно друг друга. Таким образом, мы получаем поведение undefined, так как оценка E2 ( == ++i + 1) инициирует побочный эффект (обновление i).

1.9p15:

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


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

(2) LHS присваивания также является lvalue, поэтому его оценка значения не предполагает выборку текущего значения i

выборка значения может быть частью оценки prvalue. Но в E + = F единственное prvalue равно F, поэтому выборка E не является частью оценки (lvalue) подвыражения E

Если выражение lvalue или rvalue ничего не говорит о том, как это выражение должно быть оценено. Некоторые операторы требуют lvalues ​​в качестве своих операндов, некоторые другие требуют rvalues.

Пункт 5p8:

Всякий раз, когда выражение glvalue появляется как операнд оператора, ожидающего prvalue для этого операнда, значение lvalue-to-rvalue (4.1), массив-указатель (4.2) или функция-to-pointer (4.3) стандартные преобразования применяются для преобразования выражения в prvalue.

В простом присваивании оценка LHS требует только определения идентичности объекта. Но в составном присвоении, таком как +=, LHS должен быть изменяемым значением l, но оценка LHS в этом случае состоит в определении идентичности объекта и преобразовании lvalue-to-rvalue. Это результат этого преобразования (которое является prvalue), которое добавляется к результату (также prvalue) оценки RHS.

"Но в E + = F единственное prvalue равно F, поэтому выборка E не является частью оценки подвыражения (lvalue) E"

Это не так, как я объяснил выше. В вашем примере F - выражение prvalue, но F также может быть выражением lvalue. В этом случае преобразование lvalue-rvalue также применяется к F. 5.17p7, как цитируется выше, говорит нам, что такое семантика сложных операторов присваивания. В стандарте указано, что поведение E += F совпадает с E = E + F, но E оценивается только один раз. Здесь оценка E включает преобразование lvalue-to-rvalue, потому что двоичный оператор + требует, чтобы операнды были rvalues.

Ответ 5

С точки зрения писателя компилятора они не заботятся о "i += ++i + 1", потому что независимо от того, что делает компилятор, программист может не получить правильный результат, но они, безусловно, получают то, что заслуживают. И никто не пишет такой код. О чем компилятор пишет:

*p += ++(*q) + 1;

Код должен читать *p и *q, увеличить *q на 1 и увеличить *p на некоторую сумму, которая рассчитывается. Здесь автор компилятора заботится об ограничениях на порядок операций чтения и записи. Очевидно, если p и q указывают на разные объекты, порядок не имеет значения, но если p == q, то это будет иметь значение. Опять же, p будет отличаться от q, если программист, пишущий код, не сумасшедший.

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

Таким образом, даже если поведение определено в С++ 11, было бы очень опасно использовать его, потому что (a) компилятор не может быть изменен из поведения С++ 03 и (b) он может быть undefined в С++ 14, по причинам выше.