[C++] В C ++ 11, имеет ли `i + = ++ i + 1` неопределенное поведение?


Answers

Выражение:

i += ++i + 1

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

i = ++i + 1 ;

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

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

i = ++i + 1;

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

Логика, используемая в отчете, такова:

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

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

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

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

++i + 1

чтобы:

i + ++i + 1

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

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

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

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

Прагматичным способом показать это будет использование 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; 
Question

Этот вопрос возник во время чтения (ответы). Так почему i = ++ i + 1 четко определен в C ++ 11?

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

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

Является ли оценка выражения i += ++i + 1 причиной неопределенного поведения?

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

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

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

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

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

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

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

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




Там нет четкого случае Неопределенное поведение здесь

Конечно, аргумент приводит к UB может быть дан, как я указал в вопросе, и который был повторен в ответах до сих пор. Однако это предполагает строгое чтение 5.17: 7 , которая является одновременно внутренне противоречивым и в противоречии с явными заявлениями в 5.17: 1 о назначении соединения. С более слабым чтением 5.17: 7 противоречия исчезают, как это делает аргумент для UB. Отсюда мой вывод ни что есть UB здесь, ни что там четко определено поведение, но текст стандарта противоречива, и должен быть изменен , чтобы сделать превалирует ясно , какое чтение(И я полагаю, это означает, что отчет дефект должен быть написан). Конечно, можно было бы ссылаться здесь положение отката в стандарте (примечание в 1.3.24), что оценки, для которых стандарт не определяют поведение [однозначно и самосогласованные] являются Неопределенным поведением, но это сделало бы любое использование сложных заданий (в том числе операторов префикс инкремента / декремента) в УБ, то, что может обратиться к некоторым разработчикам, но, конечно, не для программистов.

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

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

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

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

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

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

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

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

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

Этот язык далеко не так точно , как я хотел бы , чтобы это было, но я не думаю , что это натяжка предположить , что «неопределенно-секвенировал» должен означать «по отношению к этой операции присвоения соединения». (Не нормативное, я знаю) примечание становится ясно , что именующими к Rvalue преобразованием является частью операции присвоения соединения. Теперь вызов fнеопределенно-секвенировали относительно операции составного присвоения +=? Я не уверен, потому что «последовательность» отношение определяется для отдельных расчетов стоимости и побочных эффектов, а не полные оценок операторов, которые могут быть связаны как. На самом деле оценка оператора присваивания соединения включает в себя трипункты: конверсия именующих к Rvalue его левого операнда, побочный эффект (присвоение собственно), и вычислению значения задания соединения (которое секвенировало после того, как побочный эффект, и возвращает исходный левый операнд , как именующие). Заметим , что существование преобразования именующее-к-Rvalue никогда явно упоминается в стандарте , за исключением записки цитированной выше ; в частности, стандарт не делает (другое) заявления вообще о его секвенирование по сравнению с другими оценками. Это довольно очевидно , что в данном примере вызов fсеквенировал перед тем Побочный эффект и значения вычисления +=(так как вызов происходит в вычислении значения правого операнда+=), Но это может быть неопределенно-секвенировали относительно преобразования части именующих-к-Rvalue. Я помню из моего вопроса, так как левый операнд +=является именующим (и обязательно так), нельзя истолковывать именующие к Rvalue преобразования имело место как часть вычисления значения левого операнда.

Тем не менее, по принципу исключенного третьего, вызов fдолжен быть либо неопределенно-секвенировали по отношению к операции составного присвоения +=или не-секвенировали неопределенно по отношению к нему; в последнем случае оно должно быть секвенировано перед тем , потому что это не может быть секвенировано после него (призыва fбыть секвенированы перед побочным эффектом +=, а отношение быть анти-симметричным). Таким образом , первым предположить , что это является неопределенно-секвенировали относительно операции. Тогда процитированная статья говорит , что WRT призыв fоценки+=это одна операции, и записка объясняет , что это означает , что вызов не должен вмешиваться между преобразованием именующего-к-Rvalue и побочным эффектом , связанным с +=; она должна быть либо секвенировали перед тем как, или после того, как. Но , будучи секвенировали после того , как побочный эффект не представляется возможным, поэтому он должен быть перед обоими. Это делает (по транзитивности) побочный эффект ++секвенировал перед именующим-к-Rvalue преобразования, выход UB. Далее предположим , вызов fсеквенировали до операции +=. Тогда , в частности , секвенирована перед преобразованием именующих-к-Rvalue, и снова транзитивностью , так это побочный эффект ++; нет UB в этой отрасли тоже.

Вывод: 5,17: 1 противоречит 5,17: 7, если последнему берутся (CAUB), чтобы быть нормативными по вопросам UB в результате unsequenced оценок на 1,9: 15. Как я уже сказал CAUB противоречиво, а (аргументы указанных в вопросе), но этот ответ получает долго, так что я оставлю его на это сейчас.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Для того чтобы получить , что выборки в compount присваивания будет unsequenced относительно вычисления значения правого операнда, что делает наш пример UB, самый ясным способ , как представляется, с получением соединения операторов неявного третьим (средний) операнда , A prvalue , не представлены отдельное выражение, но полученный именующим к Rvalue преобразования из левого операнда (это три операнда природа соответствует расширенной форме сложных заданий, но путем получения среднего операнда из левого операнда, обеспечиваются что значение извлекается из того же объекта , к которому результат будет сохранен, решающими гарантии , что лишь неопределенно и неявно дано в текущей формулировке через « за исключением того, чтоE1вычисляются только один раз»положение). Разница с предыдущим решением является то , что выборка теперь подлинное именующего-к-Rvalue преобразование (с серединой операндами являются prvalue) и выполняются как часть вычисления значений операндов в назначение соединения , что делает его естественный unsequenced с вычислением значения правого операнда. это должно быть указано где - то (в новом пункте , который описывает этот неявный операнд) , что значение вычисление левого операнда секвенировало перед этим Lvalue-to преобразование Rvalue (ясно сусло) Сейчас 1,9:. 12 можно оставить как есть, и вместо 5.17: 7 Я предлагаю

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

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

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