Що зробив i = i ++ + 1; легальний на C ++ 17?


186

Перш ніж почати кричати невизначену поведінку, це чітко вказано в N4659 (C ++ 17)

  i = i++ + 1;        // the value of i is incremented

Ще в N3337 (C ++ 11)

  i = i++ + 1;        // the behavior is undefined

Що змінилося?

З того, що я можу зібрати, з [N4659 basic.exec]

За винятком випадків, в яких оцінюються операнди окремих операторів і субекспресії окремих виразів не є наслідком. [...] Обчислення значень операндів оператора секвенуються перед обчисленням значення результату оператора. Якщо побічний ефект на розташування пам'яті не є наслідком щодо іншого побічного ефекту на те саме місце пам'яті або обчислення значення, використовуючи значення будь-якого об'єкта в тому самому місці пам'яті, і вони потенційно не є одночасним, поведінка не визначена.

Де значення визначено в [N4659 basic.type]

Для тривіально копіюваних типів представлення значення - це набір бітів в представленні об'єкта, що визначає значення , яке є одним дискретним елементом визначеного реалізацією набору значень

Від [N3337 basic.exec]

За винятком випадків, в яких оцінюються операнди окремих операторів і субекспресії окремих виразів не є наслідком. [...] Обчислення значень операндів оператора секвенуються перед обчисленням значення результату оператора. Якщо побічний ефект на скалярний об’єкт не є наслідком щодо іншого побічного ефекту на той самий скалярний об'єкт або обчислення значення, використовуючи значення того ж скалярного об'єкта, поведінка не визначена.

Так само значення визначається в [N3337 basic.type]

Для тривіально копіюваних типів представлення значення - це набір бітів у представленні об'єкта, що визначає значення , яке є одним дискретним елементом визначеного реалізацією набору значень.

Вони ідентичні, за винятком згадки про одночасність, яка не має значення, і з використанням місця пам'яті замість скалярного об'єкта , де

Арифметичні типи, типи перерахування, типи вказівників, вказівники на типи членів std::nullptr_tта версії цих типів, кваліфіковані cv, називаються спільно скалярними типами.

Що не впливає на приклад.

Від [N4659 expr.ass]

Оператор присвоєння (=) і оператори присвоєння складної групи всі групи справа наліво. Усі вимагають модифікованого lvalue як їх лівого операнда і повертають значення lvalue, що посилається на лівий операнд. Результатом у всіх випадках є бітове поле, якщо лівий операнд є бітовим полем. У всіх випадках призначення присвоюється послідовно після обчислення значення правого і лівого операндів і перед обчисленням значення виразу призначення. Правий операнд секвенується перед лівим операндом.

Від [N3337 expr.ass]

Оператор присвоєння (=) і оператори присвоєння складної групи всі групи справа наліво. Усі вимагають модифікованого lvalue як їх лівого операнда і повертають значення lvalue, що посилається на лівий операнд. Результатом у всіх випадках є бітове поле, якщо лівий операнд є бітовим полем. У всіх випадках призначення присвоюється послідовно після обчислення значення правого і лівого операндів і перед обчисленням значення виразу призначення.

Єдина відмінність - останнє речення, яке відсутнє в N3337.

Останнє речення, однак, не повинно мати жодного значення, оскільки лівий операнд iне є ні "іншим побічним ефектом", ні "використанням значення того ж скалярного об'єкта", оскільки вираз id є значенням.


23
Ви визначили причину, чому: У C ++ 17 правий операнд секвенується перед лівим операндом. У С ++ 11 такого послідовності не було. Яке саме ваше питання?
Robᵩ

4
@ Robᵩ Дивіться останнє речення.
Перехожий

7
Хтось має посилання на мотивацію цієї зміни? Я хотів би статичний аналізатор мати можливість сказати "ти цього не хочеш робити", стикаючись з таким кодом i = i++ + 1;.

7
@NeilButterworth, це з статті p0145r3.pdf : "Уточнення порядку оцінки виразів для ідіоматичного C ++".
xaizek

9
@NeilButterworth, розділ №2 говорить, що це протилежно інтуїтивно, і навіть фахівці не вдається зробити все правильно у всіх випадках. Це майже вся їх мотивація.
xaizek

Відповіді:


144

У C ++ 11 акт "присвоєння", тобто побічний ефект модифікації LHS, секвенується після обчислення значення правого операнда. Зауважте, що це порівняно "слабка" гарантія: вона виробляє послідовності лише стосовно обчислення значень РЗС. Це нічого не говорить про побічні ефекти, які можуть бути присутніми в РЗС, оскільки поява побічних ефектів не є частиною обчислення значення . Вимоги C ++ 11 не встановлюють відносного послідовності між актом присвоєння та будь-якими побічними ефектами РЗС. Це те, що створює потенціал для UB.

Єдиною надією в цьому випадку є будь-які додаткові гарантії, надані конкретними операторами, які використовуються в RHS. Якби RHS використовував префікс ++, властивості послідовності, характерні для форми префікса ++, зберегли б день у цьому прикладі. Але постфікс ++- це інша історія: він не дає таких гарантій. У C ++ 11 побічні ефекти =та постфікс ++закінчуються непослідовними стосовно цього прикладу. І це UB.

У C ++ 17 додаткове речення додається до специфікації оператора присвоєння:

Правий операнд секвенується перед лівим операндом.

У поєднанні з вищезазначеним це дає дуже сильну гарантію. Він послідовує все, що відбувається в РЗС (включаючи будь-які побічні ефекти), перш ніж все, що відбувається в ЛГС. Оскільки фактичне призначення секвенується після LHS (і RHS), то додаткове секвенування повністю ізолює акт присвоєння від будь-яких побічних ефектів, наявних у RHS. Це більш сильне послідовність - це те, що виключає вищезгаданий UB.

(Оновлено для врахування коментарів @John Bollinger.)


3
Чи дійсно правильно включати "фактичний акт присвоєння" до ефектів, охоплених "лівим операндом" у цьому уривку? Стандарт має окрему мову про послідовність дійсного призначення. Я вважаю, що уривок, який ви представили, обмежений за обсягом послідовності лівих та правих під вирази, що, здається, недостатньо в поєднанні з рештою цього розділу, щоб підтримати добре, визначеність виступу ОП.
Джон Боллінгер

11
Виправлення: фактичне призначення все ще секвенується після обчислення значення лівого операнда, а оцінка лівого операнда секвенується після (повної) оцінки правого операнда, тому так, ця зміна є достатньою для підтримки чітко визначеної ОП запитав о. Тоді я просто хизуюсь деталями, але вони мають значення, оскільки вони можуть мати різний вплив на різний код.
Джон Боллінгер

3
@JohnBollinger: Мені здається цікавим, що автори Стандарту внесуть зміни, які погіршать ефективність навіть прямого генерування коду, і це історично не було необхідним, і все ж розбиратися у визначенні інших форм поведінки, відсутність яких набагато більша проблема, і які рідко створювала б якесь суттєве перешкоджання ефективності.
supercat

1
@Kaz: У складених завданнях проведення оцінки лівого значення після правого боку дозволяє x -= y;обробляти щось на зразок , mov eax,[y] / sub [x],eaxа не mov eax,[x] / neg eax / add eax,[y] / mov [x],eax. Я нічого не бачу в цьому. Якщо потрібно було вказати впорядкування, найефективнішим замовленням, ймовірно, було б виконати всі обчислення, необхідні спочатку для ідентифікації лівого об’єкта, а потім оцінити правий операнд, потім значення лівого об'єкта, але для цього потрібно мати термін за акт вирішення ідентичності лівого об’єкта.
Supercat

1
@Kaz: Якби xі yбули volatile, це матиме побічні ефекти. Крім того, ті самі міркування стосуватимуться і тих x += f();, де вони f()модифікуються x.
Supercat

33

Ви визначили нове речення

Правий операнд секвенується перед лівим операндом.

і ви правильно визначили, що оцінка лівого операнда як значення не має значення. Однак попередньо секвенсоване вказано як перехідне відношення. Таким чином, повний правий операнд (включаючи пост-інкремент) також секвенується перед призначенням. У C ++ 11 перед розрахунком секвенували лише обчислення значення правого операнда.


7

У старих стандартах C ++ та в C11 визначення тексту оператора присвоєння закінчується текстом:

Оцінки операндів не є наслідком.

Це означає, що побічні ефекти в операндах не є наслідком, а тому, безумовно, не визначеною поведінкою, якщо вони використовують ту саму змінну.

Цей текст було просто видалено в C ++ 11, залишивши його дещо неоднозначним. Це UB чи ні? Це було уточнено в C ++ 17, де вони додали:

Правий операнд секвенується перед лівим операндом.


Як зауваження, навіть у старих стандартах все це було зрозуміло, наприклад, із C99:

Порядок оцінки операндів не визначений. Якщо робиться спроба змінити результат оператора присвоєння або отримати доступ до нього після наступної точки послідовності, поведінка не визначена.

В основному в C11 / C ++ 11 вони заплуталися, коли видалили цей текст.


1

Це додаткова інформація до інших відповідей, і я розміщую її, як часто запитують код нижче .

Пояснення в інших відповідях є правильним, а також стосується наступного коду, який зараз добре визначений (і не змінює збережене значення i):

i = i++;

Це + 1червона оселедець, і не зовсім зрозуміло, чому Стандарт використовував її у своїх прикладах, хоча я пам’ятаю, що люди сперечаються у списках розсилки до C ++ 11, що, можливо, + 1змінилися через примушування раннього перетворення значення на праворуч, сторона руки. Звичайно, жодне з цього не застосовується в C ++ 17 (і, ймовірно, ніколи не застосовується в будь-якій версії C ++).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.