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

Этот вопрос возник во время чтения (ответы). Так почему 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 в этом примере, как я пытался объяснить. Я признаю, что соблазнительно сделать эквивалентность авторитетной (так что составные назначения становятся своего рода примитивами второго classа, смысл которых задается переписанием в терминах первоclassных примитивов, поэтому упрощение определения языка), но там являются довольно вескими аргументами против этого:

  • Эквивалентность не является абсолютной, поскольку исключение « 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.

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

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

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

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

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

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

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

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

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

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

(2) LHS присваивания также является lvalue, поэтому его оценка стоимости не предполагает выборку текущего значения i ; в то время как вычисление этого значения не зависит от вычисления значения 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 и 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 связано с неопределенным поведением?

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

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

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

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


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

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

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

В общем случае хранилище и «параллельная» загрузка – это гонка данных, что приводит к неопределенному поведению.


Обращение к добавлению

используя фиктивный

оператора для определения нецелесообразных оценок, можно попытаться определить 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.

Пусть Ei и F++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 выполняемую по отношению к преобразованию lvalue-to-r в i => UB.

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

Я бы сказал, что это избыточность. Переписывание из 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() безусловно, необходимо оценить до присвоение += .

Выражение:

 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; 

Да, это UB!

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

 i += ++i + 1 

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

5.17p1 (C ++ 11) состояния (подчеркивает мое):

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

Что означает «вычисление стоимости»?

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

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

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

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

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

1.9p15:

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


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

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

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

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

Пункт 5p8:

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

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

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

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

Здесь нет четкого примера для Undefined Behavior

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

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

 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 не зависит от побочного эффекта операции ++ , которая включает в себя один и тот же скалярный объект (1.9: 15), а вы «мертв»).

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

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

Этот язык далек от столь же точного, насколько мне бы хотелось, но я не думаю, что это просто предположить, что «неопределенно-секвенированный» должен означать «в отношении этой операции сложного назначения». Замечание (не нормативное, я знаю) дает понять, что преобразование lvalue-to-rval является частью операции составного присвоения. Теперь зов f неопределенно-секвенирован относительно операции составного присвоения += ? I’m unsure, because the ‘sequenced’ relation is defined for individual value computations and side effects, not complete evaluations of operators, which may involve both. In fact the evaluation of a compound assignment operator involves three items: the lvalue-to-rvalue conversion of its left operand, the side effect (the assignment proper), and the value computation of the compound assignment (which is sequenced after the side effect, and returns the original left operand as lvalue). Note that the existence of the lvalue-to-rvalue conversion is never explicitly mentioned in the standard except in the note cited above ; in particular, the standard makes no (other) statement at all regarding its sequencing relative to other evaluations. It is pretty clear that in the example the call of f is sequenced before the side effect and value computation of += (since the call occurs in the value computation of the right operand to += ), but it might be indeterminately-sequenced with respect to the lvalue-to-rvalue conversion part. I recall from my question that since the left operand of += is an lvalue (and necessarily so), one cannot construe the lvalue-to-rvalue conversion to have occurred as part of the value computation of the left operand.

However, by the principle of the excluded middle, the call to f must either be indeterminately-sequenced with respect to the operation of the compound assignment of += , or not indeterminately-sequenced with respect to it; in the latter case it must be sequenced before it because it cannot possibly be sequenced after it (the call of f being sequenced before the side effect of += , and the relation being anti-symmetric). So first assume it is indeterminately-sequenced with respect to the operation. Then the cited clause says that wrt the call of f the evaluation of += is a single operation, and the note explains that it means the call should not intervene between the lvalue-to-rvalue conversion and the side effect associated with += ; it should either be sequenced before both, or after both. But being sequenced after the side effect is not possible, so it should be before both. This makes (by transitivity) the side effect of ++ sequenced before the lvalue-to-rvalue conversion, exit UB. Next assume the call of f is sequenced before the operation of += . Then it is in particular sequenced before the lvalue-to-rvalue conversion, and again by transitivity so is the side effect of ++ ; no UB in this branch either.

Conclusion: 5.17:1 contradicts 5.17:7 if the latter is taken (CAUB) to be normative for questions of UB resulting from unsequenced evaluations by 1.9:15. As I said CAUB is self-contradictory as well (by arguments indicated in the question), but this answer is getting to long, so I’ll leave it at this for now.

Three problems, and two proposals for resolving them

Trying to understand what the standard writes about these matters, I distinguish three aspects in which the text is hard to interpret; they all are of a nature that the text is insufficiently clear about what model its statements are referring to. (I cite the texts at the end of the numbered items, since I do not know the markup to resume a numbered item after a quote)

  1. The text of 5.17:7 is of an apparent simplicity that, although the intention is easy to grasp, gives us little hold when applied to difficult situations. It makes a sweeping claim (equivalent behavior, apparently in all aspects) but whose application is thwarted by the exception clause. What if the behavior of E1 = E1 op E2 is undefined? Well then that of E1 op = E2 should be as well. But what if the UB was due to E1 being evaluated twice in E1 = E1 op E2 ? Then evaluating E1 op = E2 should presumably not be UB, but if so, then defined as what? This is like saying “the youth of the second twin was exactly like that of the first, except that he did not die at childbirth.” Frankly, I think this text, which has little evolved since the C version “A compound assignment of the the form E1 op = E2 differs from the simple assignment expression E1 = E1 op E2 only in that the lvalue E1 is evaluated only once.” might be adapted to match the changes in the standard.

    (5.17) 7 The behavior of an expression of the form E1 op = E2 is equivalent to E1 = E1 op E2 except that E1 is evaluated only once.[…]

  2. It is not so clear what precisely the actions (evaluations) are between which the ‘sequenced’ relation is defined. It is said (1.9:12) that evaluation of an expression includes value computations and initiation of side effects. Though this appears to say that an evaluation may have multiple (atomic) components, the sequenced relation is actually mostly defined (eg in 1.9:14,15) for individual components, so that it might be better to read this as that the notion of “evaluation” encompasses both value computations and (initiation of) side effects. However in some cases the ‘sequenced’ relation is defined for the (entire) execution of an expression of statement (1.9:15) or for a function call (5.17:1), even though a passage in 1.9:15 avoids the latter by referring directly to executions in the body of a called function.

    (1.9) 12 Evaluation of an expression (or a sub-expression) in general includes both value computations (…) and initiation of side effects. […] 13 Sequenced before is an asymmetric, transitive, pair-wise relation between evaluations executed by a single thread […] 14 Every value computation and side effect associated with a full-expression is sequenced before every value computation and side effect associated with the next full-expression to be evaluated. […] 15 When calling a function (whether or not the function is inline), every value computation and side effect associated with any argument expression, or with the postfix expression designating the called function, is sequenced before execution of every expression or statement in the body of the called function. […] Every evaluation in the calling function (including other function calls) … is indeterminately sequenced with respect to the execution of the called function […] (5.2.6, 5.17) 1 … With respect to an indeterminately-sequenced function call, …

  3. The text should more clearly acknowledge that a compound assignment involves, in contrast to a simple assignment, the action of fetching the value previously assigned to its left operand; this action is like lvalue-to-rvalue conversion, but does not happen as part of the value computation of that left operand, since it is not a prvalue; indeed it is a problem that 1.9:12 only acknowledges such action for prvalue evaluation . In particular the text should be more clear about which ‘sequenced’ relations are given for that action, if any.

    (1.9) 12 Evaluation of an expression… includes… value computations (including determining the identity of an object for glvalue evaluation and fetching a value previously assigned to an object for prvalue evaluation)

The second point is the least directly related to our concrete question, and I think it can be solved simply by choosing a clear point of view and reformulating pasages that seem to indicate a different point of view. Given that one of the main purposes of the old sequence points, and now the ‘sequenced’ relation, was to make clear that the side effect of postfix-increment operators is unsequenced wrt to actions sequenced after the value computation of that operator (thus giving eg i = i++ UB), the point of view must be that individual value computations and (initiation of) individual side effects are “evaluations” for which “sequenced before” may be defined. For pragmatic reasons I would also include two more kinds of (trivial) “evaluations”: function entry (so that the language of 1.9:15 may be simplified to: “When calling a function…, every value computation and side effect associated with any of its argument expressions, or with the postfix expression designating the called function, is sequenced before entry of that function”) and function exit (so that any action in the function body gets by transitivity sequenced before anything that requires the function value; this used to be guaranteed by a sequence point, but the C++11 standard seems to have lost such guarantee; this might make calling a function ending with return i++; potentially UB where this is not intended, and used to be safe). Then one can also be clear about the “indeterminately sequenced” relation of functions calls: for every function call, and every evaluation that is not (directly or indirectly) part of evaluating that call, that evaluation shall be sequenced (either before or after) wrt both entry and exit of that function call, and it shall have the same relation in both cases (so that in particular such external actions cannot be sequenced after function entry but before function exit, as is clearly desirable within a single thread).

Now to resolve points 1. and 3., I can see two paths (each affecting both points), which have different consequences for the defined or not behavior of our example:

Compound assignments with two operands, and three evaluations

Compound operations have thier two usual operands, an lvalue left operand and a prvalue right operand. To settle the unclarity of 3., it is included in 1.9:12 that fetching the value previously assigned to an object also may occur in compound assignments (rather than only for prvalue evaluation). The semantics of compount assignments are defined by changing 5.17:7 to

In a compound assignment op = , the value previously assigned to the object referred to by the left operand is fetched, the operator op is applied with this value as left operand and the right operand of op = as right operand, and the resulting value replaces that of the object referred to by the left operand.

(That gives two evaluations, the fetch and the side effect; a third evaluation is the trivial value computation of the compound operator, sequenced after both other evaluations.)

For clarity, state clearly in 1.9:15 that value computations in operands are sequenced before all value computations associated with the operator (rather than just those for the result of the operator ), which ensures that evaluating the lvalue left operand is sequenced before fetching its value (one can hardly imagine otherwise), and also sequences the value computation of the right operand before that fetch, thus excluding UB in our example. While at it, I see no reason not to also sequence value computations in operands before any side effects associated with the operator (as they clearly must); this would make mentioning this explicitly for (compound) assignments in 5.17:1 superfluous. On the other hand do mention there that the value fetching in a compound assignment is sequenced before its side effect.

Compound assignments with three operands, and two evaluations

In order to obtain that the fetch in a compount assignment will be unsequenced with respect to the value computation of the right operand, making our example UB, the clearest way seems to be to give compound operators an implicit third (middle) operand , a prvalue, not represented by a separate expression, but obtained by lvalue-to-rvalue conversion from the left operand (this three-operand nature corresponds to the expanded form of compound assignments, but by obtaining the middle operand from the left operand, it is ensured that the value is fetched from the same object to which the result will be stored, a crucial guarantee that is only vaguely and implicitly given in the current formulation through the “except that E1 is evaluated only once” clause). The difference with the previous solution is that the fetch is now a genuine lvalue-to-rvalue conversion (since the middle operand is a prvalue) and is performed as part of the value computation of the operands to the compound assignment , which makes it naturally unsequenced with the value computation of the right operand. It should be stated somewhere (in a new clause that describes this implicit operand) that the value computation of the left operand is sequenced before this lvalue-to-rvalue conversion (it clearly must). Now 1.9:12 can be left as it is, and in place of 5.17:7 I propose

In a compound assignment op = with left operand a (an lvalue), and midlle and right operands b respectively c (both prvalues), the operator op is applied with b as left operand and c as right operand, and the resulting value replaces that of the object referred to by a .

(That gives one evaluation, the side effect, with as second evaluation the trivial value computation of the compound operator, sequenced after it.)

The still applicable changes to 1.9:15 and 5.17:1 suggested in the previous solution could still apply, but would not give our original example defined behavior. However the modified example at the top of this answer would still have defined behavior, unless the part 5.17:1 “compound assignment is a single operation” is scrapped or modified (there is a similar passage in 5.2.6 for postfix increment/decrement). The existence of those passages would suggest that detaching the fecth and store operations within a single compound assignement or postfix increment/decrement was not the intention of those who wrote the current standard (and by extension making our example UB), but this of course is mere guesswork.

From the compiler writer’s perspective, they don’t care about "i += ++i + 1" , because whatever the compiler does, the programmer may not get the correct result, but they surely get what they deserve. And nobody writes code like that. What the compiler writer cares about is

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

The code must read *p and *q , increase *q by 1, and increase *p by some amount that is calculated. Here the compiler writer cares about restrictions on the order of read and write operations. Obviously if p and q point to different objects, the order makes no difference, but if p == q then it will make a difference. Again, p will be different from q unless the programmer writing the code is insane.

By making the code undefined, the language allows the compiler to produce the fastest possible code without caring for insane programmers. By making the code defined, the language forces the compiler to produce code that conforms to the standard even in insane cases, which may make it run slower. Both compiler writers and sane programmers don’t like that.

So even if the behaviour is defined in C++11, it would be very dangerous to use it, because (a) a compiler might not be changed from C++03 behaviour, and (b) it might be undefined behaviour in C++14, for the reasons above.

  • Псевдонимы T * с char * разрешены. Разрешено ли это другим способом?
  • Одномерный доступ к многомерному массиву: хорошо ли оно определено?
  • При использовании заголовков C в C ++ следует ли использовать функции из std :: или глобального пространства имен?
  • Имеет ли printf ("% x", 1) неопределенное поведение?
  • Является ли stl-вектор одновременным чтением streamобезопасным?
  • int a = {1,2,}; Разрешена странная запятая. Любая конкретная причина?
  • Вычисления без последствий (ака последовательности)
  • Что говорит стандарт о том, как определение четкости вектора меняет емкость?
  • Эффективный беззнаковый кран, исключающий поведение, определяемое реализацией
  • Interesting Posts

    Как сделать пунктирную / пунктирную линию в Android?

    Можно ли использовать новую файловую систему ReFS в Windows 8?

    Соединение по отсутствующим значениям с помощью geom_line

    Как выполнить команду очистки на сервере tmux / сеансе?

    Ошибка с плавающей запятой в представлении?

    Как предоставить повышенные привилегии для быстрого запуска Windows 10 (для автоматического запуска WampServer)?

    Предотrotation переноса JPreeChart DialPlot с большими значениями?

    Попытка нарисовать кнопку: как установить цвет штриха и как «выровнять» gradleиент на дно, не зная высоты?

    Каталог документов iPhone и UIFileSharingEnabled, скрывающие определенные документы

    R: ускорение операций «по группам»

    MacOS X: журнал полон «попыток подключения стелс-режима» от маршрутизатора LAN

    Почему ng-scope добавлен в javascript внутри моего частичного представления и не делает предупреждение неработоспособным?

    Масштабировать текст в режиме просмотра?

    Поддерживает ли Mongoose метод Mongodb `findAndModify`?

    Как запретить Visual Studio запускать WcfSvcHost.exe во время отладки?

    Давайте будем гением компьютера.