На практиці, чому різні компілятори обчислюють різні значення int x = ++ i + ++ i ;?


165

Розглянемо цей код:

int i = 1;
int x = ++i + ++i;

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

  1. обидва ++iповертаються 2, в результаті чого x=4.
  2. один ++iповертається, 2а інший повертається 3, в результаті чого x=5.
  3. обидва ++iповертаються 3, в результаті чого x=6.

Мені друге здається найбільш вірогідним. Один із двох ++операторів виконується за допомогою i = 1, iпримножується та 2повертається результат . Потім другий ++оператор виконується за допомогою i = 2, iпримножується і 3повертається результат . Потім 2і 3складаються разом, щоб дати 5.

Однак я запустив цей код у Visual Studio, і результат був 6. Я намагаюся краще зрозуміти компілятори, і мені цікаво, що може призвести до результату 6. Єдине припущення полягає в тому, що код може бути виконаний за допомогою вбудованої паралельності. Два++ операторів, кожен збільшився iдо повернення іншого, а потім обидва повернулися 3. Це суперечило б моєму розумінню стека викликів і потребувало б пояснення.

Які (розумні) речі C++компілятор може зробити, що призведе до результату 4або результату чи 6?

Примітка

Цей приклад з’явився як приклад невизначеної поведінки у програмуванні Бьярна Струструпа «Програмування: принципи та практика використання C ++» (C ++ 14).

Див. Коментар кориці .


5
Специфікація C фактично не охоплює впорядкування операцій або оцінок у правій частині = = порівняно з операціями до / після інкременту, лише зліва.
Cristobol Polychronopolis

2
Рекомендуємо цитувати запитання, якщо ви взяли цей приклад із книги Струструпа (як зазначено в коментарі до однієї з відповідей).
Даніель Р. Коллінз

4
@philipxy Запропонована вами копія не є дублікатом цього запитання. Питання різні. Відповіді у запропонованій копії не дають відповіді на це запитання. Відповіді у запропонованому вами дублікаті не є дублікатами прийнятих (або високих голосів) відповідей на це питання. Я вважаю, ви неправильно прочитали моє запитання. Пропоную вам перечитати його і переглянути голосування, щоб закрити.
кориця

3
@philipxy "Відповіді говорять про те, що компілятор може робити все ..." Це не відповідає на моє запитання. "вони показують, що навіть якщо ви думаєте, що ваше запитання інше, це просто варіація цього" Що? "хоча ви не надаєте свою версію C ++" Моя версія C ++ не має відношення до мого запитання. "тому вся програма, в якій знаходиться заява, може взагалі зробити що завгодно". Я знаю, але моє запитання стосувалось конкретної поведінки. "Ваш коментар не відображає змісту відповідей там." Мій коментар відображає зміст мого запитання, яке вам слід перечитати.
кориця

2
Відповісти на заголовок; тому що UB означає, що поведінка невизначена. Кілька компіляторів, створених у різний час історії різними людьми для різних архітектур, коли їм пропонувалося розфарбувати поза рядків та здійснити реальну світову реалізацію, вони повинні були помістити щось у цю частину поза специфікацією, тому люди робили саме це, і кожен з них використовували різні кольорові олівці. Звідси мізерна максима, не покладайтесь на УБ
Тобі

Відповіді:


200

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

Код

int i = 1;
int x = ++i + ++i;

складається з наступних інструкцій:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

Але, незважаючи на те, що це нумерований список, як я його написав, існує лише декілька залежностей впорядкування : 1-> 2-> 3-> 4-> 5-> 10-> 11 та 1-> 6-> 7- > 8-> 9-> 10-> 11 повинні залишатися у своєму відносному порядку. Крім цього, компілятор може вільно змінювати порядок і, можливо, усувати надмірність.

Наприклад, ви можете замовити список таким чином:

1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
4. store tmp1 in i
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

Чому компілятор може це зробити? Оскільки немає послідовності побічних ефектів приросту. Але тепер компілятор може спростити: наприклад, мертвий магазин в 4: значення відразу перезаписується. Крім того, tmp2 і tmp4 - це насправді одне і те ж.

1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

І тепер усе, що стосується tmp1, є мертвим кодом: він ніколи не використовується. І перечитання i теж можна усунути:

1. store 1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
10. add tmp3 and tmp3, as tmp5
11. store tmp5 in x

Подивіться, цей код набагато коротший. Оптимізатор задоволений. Програміст - ні, тому що я був збільшений лише один раз. На жаль

Давайте подивимось на щось інше, що може зробити замість цього компілятор: повернемось до початкової версії.

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

Компілятор може змінити порядок так:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

а потім знову зауважте, що я читається двічі, тож усуньте одне з них:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

Це приємно, але це може піти далі: воно може повторно використовувати tmp1:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

Тоді це може усунути перечитання i в 6:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

Зараз 4 - мертвий магазин:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

і тепер 3 і 7 можна об’єднати в одну інструкцію:

1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

Виключити останнє тимчасове:

1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
10. add tmp1 and tmp1, as tmp5
11. store tmp5 in x

І тепер ви отримуєте результат, який дає вам Visual C ++.

Зверніть увагу, що в обох шляхах оптимізації важливі залежності від замовлення збереглися, оскільки інструкції не були видалені за бездіяльність.


36
Наразі це єдина відповідь, в якій згадується послідовність .
PM 2Ring

3
-1 Я не думаю, що ця відповідь є уточнюючою. Спостережувані результати взагалі не залежать від будь-якої оптимізації компілятора (див. Мою відповідь).
Даніель Р. Коллінз,

3
Це передбачає операції читання-модифікації-запису. Деякі процесори, такі як всюдисущий x86, мають операцію атомного збільшення, що додає ситуації ще більшої складності.
Марк

6
@philipxy "Стандарт не має нічого сказати про об'єктний код." Стандарт також не має чого сказати про поведінку цього фрагмента - це UB. Це передумова питання. ОП хотіла знати, чому на практиці укладачі можуть отримати різні та дивні результати. Крім того, моя відповідь навіть нічого не говорить про об’єктний код.
Себастьян Редл,

5
@philipxy Я не розумію твого заперечення. Як уже зазначалося, питання полягає в тому, що компілятор може робити у присутності UB, а не в стандарті C ++. Чому використання об’єктного коду було б недоречним під час вивчення того, як гіпотетичний компілятор перетворює код? Насправді, як би щось, крім об'єктного коду, було доречним?
Конрад Рудольф

58

Хоча це UB (як мається на увазі OP), наступні гіпотетичні способи, якими компілятор міг отримати 3 результати. Усі три дали б однаковий правильний xрезультат, якщо використовувати їх із різними int i = 1, j = 1;змінними замість однієї і тієї ж i.

  1. обидва ++ i повертають 2, в результаті х = 4.
int i = 1;
int i1 = i, i2 = i;   // i1 = i2 = 1
++i1;                 // i1 = 2
++i2;                 // i2 = 2
int x = i1 + i2;      // x = 4
  1. один ++ i повертає 2, а інший - 3, в результаті х = 5.
int i = 1;
int i1 = ++i;           // i1 = 2
int i2 = ++i;           // i2 = 3
int x = i1 + i2;        // x = 5
  1. обидва ++ i повертають 3, в результаті х = 6.
int i = 1;
int &i1 = i, &i2 = i;
++i1;                   // i = 2
++i2;                   // i = 3
int x = i1 + i2;        // x = 6

2
це краща реакція, ніж я сподівався, дякую.
кориця

1
Для варіанту 1 компілятор міг зробити примітку до попереднього збільшення i. Знаючи, що це може статися лише один раз, це випускає лише один раз. Для варіанту 2 код перекладається на машинний код буквально, як це може зробити проект класу компілятора коледжу. Для варіанту 3 це як варіант 1, але він зробив дві копії преінкременту. Потрібно було використовувати вектор, а не набір. :-)
Zan Lynx

@dxiv вибачте, погано, я переплутав пости
муру

22

Мені друге здається найбільш вірогідним.

Я йду на варіант №4: Обидва ++iтрапляються одночасно.

Нові процесори рухаються до цікавих оптимізацій, а паралельна оцінка коду, де це дозволено, як і тут, - це ще один спосіб, яким компілятори продовжують робити швидший код. Я бачу як практичну реалізацію компілятори, що рухаються до паралелізму.

Я міг легко побачити стан перегони, що спричиняє недетерміновану поведінку або несправність шини через одне і те ж суперечка пам'яті - все це дозволено, оскільки кодер порушив контракт C ++ - отже, UB.

Моє запитання: які (розумні) речі може зробити компілятор С ++, що призведе до результату 4 або результату або 6?

Це могло , але не рахувати в цьому.

Не використовуйте ++i + ++iі не очікуйте розумних результатів.


Якби я міг прийняти і цю відповідь, і @ dxiv, я б. Дякую за відповідь.
кориця

4
@UriRaz: Процесор може навіть не помітити, що існує загроза даних залежно від вибору компілятора. Наприклад, компілятор може призначити iдва регістри, збільшити обидва регістри та записати обидва назад. Процесор не може вирішити це. Принципова проблема полягає в тому, що ні C ++, ні сучасні процесори не є суворо послідовними. С ++ явно має послідовність подій "до" та "після", щоб за замовчуванням дозволити "відбувається в той же час".
MSalters

1
Але ми знаємо, що це не так для OP, що використовує Visual Studio; більшість основних ISA, включаючи x86 та ARM, визначаються з точки зору повністю послідовної моделі виконання, коли одна машинна інструкція повністю закінчується до початку наступної. Суперскалярний вийшов з ладу повинен підтримувати цю ілюзію для однієї нитки. (Інші потоки, що читають спільну пам’ять, не гарантовано бачать речі в порядку програми, але основне правило OoO exec - не порушувати однопотокове виконання.)
Пітер Кордес,

1
Це моя улюблена відповідь, оскільки вона єдина, де згадується паралельне виконання інструкцій на рівні процесора. До речі, було б непогано зазначити у відповіді, що або через умови гонки потік процесора буде зупинений, чекаючи розблокування мьютекса в тому самому місці пам'яті, тому це дуже неоптимально для моделі паралельності. По-друге - через однакові умови перегонів реальною відповіддю може бути 4або 5, - залежно від моделі / швидкості виконання потоку процесора, тому це UB в основі.
Агніус Василяускас

1
@AgniusVasiliauskas Можливо, все ж "На практиці, чому різні компілятори обчислюють різні значення", шукає більше простого для розуміння з огляду на спрощений погляд на сучасні процесори. Проте діапазон сценаріїв компіляторів / процесорів набагато більший, ніж кілька згаданих різних відповідей. Ваше корисне розуміння - це ще одне. ІМО, паралелізм - це майбутнє, і тому ця відповідь була зосереджена на тих, хоча і абстрактно - оскільки майбутнє все ще розгортається. IAC, публікація стала популярною, і легкі для сприйняття відповіді найкраще винагороджуються.
chux

17

Я думаю, що проста та зрозуміла інтерпретація (без жодної заявки на оптимізацію компілятора чи багатопотоковість) була б просто такою:

  1. Приріст i
  2. Приріст i
  3. Додайте i+i

При iзбільшенні в два рази його значення дорівнює 3, а якщо скласти разом, сума дорівнює 6.

Для перевірки розглянемо це як функцію C ++:

int dblInc ()
{
    int i = 1;
    int x = ++i + ++i;
    return x;   
}

Тепер ось код збірки, який я отримую від компіляції цієї функції, використовуючи стару версію компілятора GNU C ++ (win32, gcc версія 3.4.2 (mingw-special)). Тут не відбувається вигадливих оптимізацій та багатопотоковості:

__Z6dblIncv:
    push    ebp
    mov ebp, esp
    sub esp, 8
    mov DWORD PTR [ebp-4], 1
    lea eax, [ebp-4]
    inc DWORD PTR [eax]
    lea eax, [ebp-4]
    inc DWORD PTR [eax]
    mov eax, DWORD PTR [ebp-4]
    add eax, DWORD PTR [ebp-4]
    mov DWORD PTR [ebp-8], eax
    mov eax, DWORD PTR [ebp-8]
    leave
    ret

Зверніть увагу, що локальна змінна iзнаходиться в стеку лише в одному місці: адреса [ebp-4]. Це місце збільшується вдвічі (у 5-8-му рядках функції складання; включаючи, мабуть, надлишкові навантаження цієї адреси eax). Потім на 9-10-му рядках це значення завантажується eax, а потім додається в eax(тобто обчислює струм i + i). Потім воно надмірно копіюється в стек і повертається вeax як повернене значення (яке, очевидно, буде 6).

Може бути цікавим поглянути на стандарт C ++ (тут, старий: ISO / IEC 14882: 1998 (E)), який говорить про вирази, розділ 5.4:

За винятком випадків, коли зазначено, порядок оцінки операндів окремих операторів та підвиражень окремих виразів, а також порядок, у якому мають місце побічні ефекти, не визначений.

З приміткою:

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

У цей момент наводяться два приклади невизначеної поведінки, обидва за участю оператора приросту (один з яких:) i = ++i + 1.

Тепер, якщо хтось бажає, можна: Скласти цілочисельний клас обгортки (як Java Integer); функції перевантаження operator+та operator++такі, що вони повертають об'єкти проміжного значення; і тим самим напишіть ++iObj + ++iObjі отримайте його, щоб повернути об'єкт, що містить 5. (Я не включив сюди повний код заради стислості.)

Особисто я б заінтригував, якби був приклад відомого компілятора, який виконував цю роботу будь-яким іншим способом, крім послідовності, розглянутої вище. Мені здається, що найпростішою реалізацією було б просто виконати два коди збірки для incпримітивного типу перед тим, як буде виконана операція додавання.


2
Оператор збільшення дійсно має дуже чітко визначене значення "return"
edc65

@philipxy: Я відредагував відповідь, щоб видалити уривки, проти яких ти заперечував. На даний момент ви можете або не погодитись з відповіддю більше.
Даніель Р. Коллінз,

2
Це не "Два приклади невизначеної поведінки", це два приклади невизначеної поведінки , дуже різного звіра, що виникає внаслідок іншого уривку стандарту. Я бачу, як C ++ 98 у тексті прикладу виноски звикли говорити "неуточнене", що суперечить нормативному тексту, але це було виправлено пізніше.
Куббі,

@Cubbi: І текст, і виноска у наведеному тут стандарті використовують фразу "неуточнене", "не зазначене безпосередньо", і, схоже, воно відповідає терміну з розділу 1.3.13.
Даніель Р. Коллінз

1
@philipxy: Я бачу, що ти повторив той самий коментар до багатьох відповідей тут. Здається, що ваша головна критика насправді стосується самого питання ОП, сфера дії якого стосується не лише абстрактного стандарту.
Даніель Р. Коллінз

7

Розумна річ, яку може зробити компілятор, - це Усунення субекспресії. Це вже поширена оптимізація у компіляторах: якщо підвираз виглядає (x+1)кілька разів у більшому виразі, його потрібно обчислити лише один раз. Наприклад , в a/(x+1) + b*(x+1)до x+1подвираженія можна обчислити один раз.

Звичайно, компілятор повинен знати, які підвирази можна оптимізувати таким чином. Двократний дзвінок rand()повинен дати два випадкові числа. Тому невбудовані виклики функцій повинні бути звільнені від CSE. Як ви зауважили, немає правила, яке б вказувало, як i++слід обробляти два випадки , тому немає жодних підстав звільняти їх від CSE.

Результат дійсно може бути таким, який int x = ++i + ++i;оптимізований до int __cse = i++; int x = __cse << 1. (CSE, після чого повторне зниження міцності)


Стандарт не має що сказати про об’єктний код. Це не виправдано визначенням мови та не пов’язане з ним.
philipxy

1
@philipxy: Стандарт не має нічого сказати про будь-яку форму невизначеної поведінки. Це передумова питання.
MSalters

7

На практиці ви використовуєте невизначену поведінку. Може трапитися все, що завгодно, не тільки те, що ви вважаєте "розумним", і часто таке відбувається трапляються які ви не вважаєте розумними. Все за визначенням "розумно".

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

Голос проти: GCC категорично не погоджується з вами.


Коли Стандарт характеризує щось як "невизначену поведінку", це означає ні більше, ні менше, ніж поведінка, що знаходиться поза юрисдикцією Стандарту . Оскільки Стандарт не робить спроб судити про розумність речей, що не підпадають під його юрисдикцію, і не робить спроб заборонити всі способи, яким відповідне імплементація може бути безпідставно марною, невдача Стандарту встановити вимоги в конкретній ситуації не означає жодного судження про те, що можливі дії однаково "розумні".
supercat

6

Немає розумного, що компілятор міг би зробити, щоб отримати результат 6, але це можливо і правомірно. Результат 4 є цілком обґрунтованим, і я б вважав результат 5 прикордонним розумним. Всі вони є абсолютно законними.

Гей, почекай! Хіба не ясно, що має статися? Додавання потребує результатів двох приростів, тому, очевидно, вони повинні відбутися спочатку. І ми йдемо зліва направо, так що ... Ох! Якби тільки це було так просто. На жаль, це не так. Ми не йдемо зліва направо, і в цьому проблема.

Зчитування розташування пам'яті у двох регістрах (або ініціалізація їх обох з одного літералу, оптимізація зворотного шляху до пам'яті) є дуже розумною справою для компілятора. Це фактично призведе до того, що потайно існують дві різні змінні, кожна зі значенням 2, яка, нарешті, буде додана до результату 4. Це "розумно", оскільки це швидко і ефективно, і це відповідає обом стандарт і з кодом.

Подібним чином, розташування пам'яті можна прочитати один раз (або змінну, ініціалізовану з літералу) і збільшити один раз, а тіньову копію в іншому регістрі можна збільшити після цього, що призведе до додавання 2 і 3. Це, я б сказав, граничне обґрунтування, хоча і цілком законне. Я вважаю це прикордонним розумним, оскільки це не те чи інше. Це не "розумний" оптимізований спосіб, і не "розумний" точно педантичний спосіб. Це дещо посередині.

Двічі збільшити місце пам’яті (що призводить до значення 3), а потім додати це значення до себе для кінцевого результату 6 є правомірним, але не зовсім розумним, оскільки робити об’їми пам’яті не є точно ефективними. Хоча на процесорі з гарною переадресацією сховища, це цілком може бути "розумно" зробити, оскільки магазин повинен бути в основному невидимим ...
Оскільки компілятор "знає", що це одне і те ж місце, він також може вибрати збільшення значення двічі в реєстрі, а потім також додайте його до себе. Будь-який із підходів дасть вам результат 6.

За формулюванням стандарту, компілятору дозволено давати вам будь-який такий результат, хоча я особисто вважав би 6 майже пам’яткою "ебать вас" від Неприємного департаменту, оскільки це досить несподівана річ (юридична чи ні, намагатися завжди робити найменшу кількість сюрпризів - це добре зробити!). Хоча, бачачи, як пов'язана невизначена поведінка, на жаль, не можна справді сперечатися про "несподіване", так.

Отже, насправді, який код у вас є для компілятора? Давайте запитаємо clang, який покаже нам, якщо ми красиво запитаємо (викликаючи за допомогою -ast-dump -fsyntax-only):

ast.cpp:4:9: warning: multiple unsequenced modifications to 'i' [-Wunsequenced]
int x = ++i + ++i;
        ^     ~~
(some lines omitted)
`-CompoundStmt 0x2b3e628 <line:2:1, line:5:1>
  |-DeclStmt 0x2b3e4b8 <line:3:1, col:10>
  | `-VarDecl 0x2b3e430 <col:1, col:9> col:5 used i 'int' cinit
  |   `-IntegerLiteral 0x2b3e498 <col:9> 'int' 1
  `-DeclStmt 0x2b3e610 <line:4:1, col:18>
    `-VarDecl 0x2b3e4e8 <col:1, col:17> col:5 x 'int' cinit
      `-BinaryOperator 0x2b3e5f0 <col:9, col:17> 'int' '+'
        |-ImplicitCastExpr 0x2b3e5c0 <col:9, col:11> 'int' <LValueToRValue>
        | `-UnaryOperator 0x2b3e570 <col:9, col:11> 'int' lvalue prefix '++'
        |   `-DeclRefExpr 0x2b3e550 <col:11> 'int' lvalue Var 0x2b3e430 'i' 'int'
        `-ImplicitCastExpr 0x2b3e5d8 <col:15, col:17> 'int' <LValueToRValue>
          `-UnaryOperator 0x2b3e5a8 <col:15, col:17> 'int' lvalue prefix '++'
            `-DeclRefExpr 0x2b3e588 <col:17> 'int' lvalue Var 0x2b3e430 'i' 'int'

Як бачите, той самий lvalue Var 0x2b3e430префікс ++застосовано у двох місцях, і ці два знаходяться під одним вузлом у дереві, що є дуже неспеціальним оператором (+), про який не сказано нічого особливого щодо послідовності чи подібного. Чому це важливо? Ну, читайте далі.

Зверніть увагу на попередження: "безліч послідовних модифікацій" i "" . О, о, це не звучить добре. Що це означає? [basic.exec] розповідає нам про побічні ефекти та послідовності та повідомляє (параграф 10), що за замовчуванням, якщо прямо не сказано інше, оцінки операндів окремих операторів та підвиразів окремих виразів не є наслідками . Ну, боже, така справа operator+- нічого не сказано інакше, тому ...

Але чи ми дбаємо про секвенування раніше, невизначено-секвенування чи несеквенцію? Хто б хотів знати?

Цей самий параграф також повідомляє нам, що оцінки без послідовностей можуть перекриватися, і що коли вони посилаються на одне і те ж місце пам'яті (це так!), І що це не є потенційно одночасним, тоді поведінка не визначена. Тут справді стає потворно, бо це означає, що ви нічого не знаєте і не маєте жодних гарантій щодо того, щоб бути «розумним». Нерозумна річ насправді цілком допустима і "розумна".


Використання "розумного" полягало лише в тому, щоб запобігти тому, щоб хтось сказав "компілятор міг зробити ЩО-небудь, навіть видати єдину інструкцію" встановити х на 7 "", можливо, я мав би це пояснити.
кориця

@cinnamon Багато років тому, коли я був молодим і недосвідченим, інженери-компілятори Sun сказали мені, що їх компілятор діяв абсолютно розумно, створюючи код невизначеної поведінки, який я вважав нерозумним на той час. Урок вивчений.
gnasher729

Стандарт не має що сказати про об’єктний код. Це фрагментовано та незрозуміло щодо того, як запропоновані вами реалізації обґрунтовані або пов’язані з визначенням мови.
philipxy

@philipxy: Стандарт визначає, що добре сформовано та чітко визначено, а що ні. У випадку цього Q воно визначає поведінку як невизначену. Окрім законності, існує також обґрунтоване припущення, що компілятори генерують ефективний код. Так, ви маєте рацію, стандарт не вимагає, щоб це було так. Проте це обґрунтоване припущення.
Деймон

@Damon: Стандарт вказує, які дії всі реалізації потрібно обробляти, як визначено, і які дії реалізації не потрібно обробляти, як визначено. Оскільки деякі завдання вимагають ширшого діапазону семантики, ніж інші, невдача стандарту визначити поведінку певної дії не означає, що реалізація, яка її визначає, у будь-якому випадку буде непридатнішою для деяких завдань, ніж та, яка не , а також те, що невизначення такої поведінки не призведе до того, що реалізація стане менш придатною для одних цілей, ніж інші, які її визначають.
supercat

1

Є правило :

Між попередньою та наступною точкою послідовності скалярний об'єкт повинен мати збережене значення щонайменше один раз шляхом обчислення виразу, інакше поведінка не визначена.

Таким чином, навіть x = 100 є можливим дійсним результатом.

Для мене найбільш логічним результатом у прикладі є 6, оскільки ми збільшуємо значення i вдвічі, і вони додають його до себе. Важко зробити додавання перед обчисленням значень з обох сторін "+".

Але розробники компіляторів можуть реалізувати будь-яку іншу логіку.


0

Схоже, ++ i повертає значення lvalue, але i ++ повертає значення rvalue.
Отже, цей код нормальний:

int i = 1;
++i = 10;
cout << i << endl;

Цей не є:

int i = 1;
i++ = 10;
cout << i << endl;

Наведені вище два твердження відповідають VisualC ++, GCC7.1.1, CLang та Embarcadero.
Ось чому ваш код у VisualC ++ та GCC7.1.1 схожий на наступний

int i = 1;
... do something there for instance: ++i; ++i; ...
int x = i + i;

Дивлячись на розбирання, він спочатку збільшує i, переписує i. При спробі додати це робить те саме, збільшує i і переписує. Потім додає i до i. Я помітив, що CLang та Embarcadero діють по-різному. Отже, це не узгоджується з першим твердженням, після першого ++ i він зберігає результат у rvalue, а потім додає його до другого i ++.
введіть тут опис зображення


Проблема "виглядає як лінія-значення" полягає в тому, що ви говорите з точки зору стандарту C ++, а не компілятора.
MSalters

@MSalters Заява відповідає VisualStudio 2019, GCC7.1.1, clang та Embarcadero, а також першій частині коду. Тож специфікація узгоджується. Але це працює по-іншому для другої частини коду. Другий фрагмент коду узгоджується з VisualStudio 2019 та GCC7.1.1, але не узгоджується з clang та Embarcadero.
Армагедеску

3
Ну, перший фрагмент коду у вашій відповіді є законним C ++, тому, очевидно, реалізації відповідають специфікації. Порівняно із запитанням, ваше "зробити щось" закінчується крапкою з комою, роблячи його повною заявою. Це створює послідовність, яка вимагається стандартом С ++, але не присутня у питанні.
MSalters

@MSalters Я хотів зробити це як еквівалентний псевдокод. Проте я не впевнений, як це переформулювати
Армагедеску,

0

Я особисто ніколи б не очікував, що компілятор виведе 6 у вашому прикладі. На ваше запитання вже є хороші та докладні відповіді. Я спробую коротку версію.

В основному, ++i це двоступеневий процес у цьому контексті:

  1. Збільшити значення i
  2. Прочитайте значення i

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

  1. Приріст iдля лівого операнда
  2. Приріст iдля правого операнда
  3. Прочитайте iлівий операнд
  4. Прочитайте iправильний операнд
  5. Підсумуйте два: врожайність 6

Тепер, коли я думаю над цим, 6 має найбільший сенс відповідно до стандарту. Для результату 4 нам потрібен процесор, який перший читаєi самостійно, потім збільшує та записує значення назад у те саме місце; в основному расова умова. Для значення 5 нам потрібен компілятор, який представляє тимчасові.

Але стандарт говорить, що ++iзбільшує змінну до її повернення, тобто до фактичного виконання поточного рядка коду. Оператору суми +потрібно підсумовувати i + iпісля застосування приростів. Я б сказав, що C ++ повинен працювати над змінними, а не над семантикою значення. Отже, для мене 6 зараз має найбільший сенс, оскільки він спирається на семантику мови, а не на модель виконання процесорів.


0
#include <stdio.h>


void a1(void)
{
    int i = 1;
    int x = ++i;
    printf("i=%d\n",i);
    printf("x=%d\n",x);
    x = x + ++i;    // Here
    printf("i=%d\n",i);
    printf("x=%d\n",x);
}


void b2(void)
{
    int i = 1;
    int x = ++i;
    printf("i=%d\n",i);
    printf("x=%d\n",x);
    x = i + ++i;    // Here
    printf("i=%d\n",i);
    printf("x=%d\n",x);
}


void main(void)
{
    a1();
    // b2();
}

Ласкаво просимо на stackoverflow! Чи можете ви надати будь-які обмеження, припущення чи спрощення у своїй відповіді? Дивіться більш детальну інформацію про те , як відповісти на це посилання: stackoverflow.com/help/how-to-answer
Усамою Abdulrehman

0

добре, це залежить від конструкції компілятора. Тому відповідь буде залежати від способу декодування компілятора. Використання двох різних змінних ++ x та ++ y замість того, щоб створити логіку, було б кращим вибором. Примітка: виведення залежить від версії останньої версії мови в MS Visual Studio, якщо її оновлено.


0

Спробуйте це

int i = 1;
int i1 = i, i2 = i;   // i1 = i2 = 1
++i1;                 // i1 = 2
++i2;                 // i2 = 2
int x = i1 + i2;      // x = 4

0

З цього посилання порядок оцінки :

Порядок оцінки операндів будь-якого оператора C, включаючи порядок оцінки аргументів функції у виразі виклику функції та порядок оцінки підвиразів у будь-якому виразі не визначений (за винятком випадків, зазначених нижче). Компілятор оцінить їх у будь-якому порядку і може обрати інший порядок, коли той самий вираз буде оцінено знову .

З цитат ясно, що порядок оцінки не визначений стандартами С. Різні компілятори реалізують різні порядки оцінки. Компілятор може вільно оцінювати такі вирази в будь-якому порядку. Ось чому різні компілятори дають різні результати для виразу, згаданого у питанні.

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


Яке відношення має цитата та ваше твердження до відповіді на питання? (Риторичне.) У будь-якому випадку даний код має невизначену поведінку, як це пояснюється в іншому місці, тому ваші пункти не застосовуються. Крім того, запитувач намагається запитати (погано) про те, які аспекти їх реалізації призводять до такого, що він робить, враховуючи, що код невизначений. Крім того, вони не погодиться з тим, що без роз'яснення їх питання щось "розумно" робити компілятор. Також ця публікація нічого не додає до публікацій, які вже тут.
Філіпсія

Ваш коментар не стосується жодної з моїх проблем. У тому числі, що код невизначений і не вказаний.
Філіпсія

У цитатах не згадується невизначене, вони згадуються невстановлене. Я все.
Філіпсія

Так, це не визначено, тому різний компілятор передбачає різні порядки оцінки. Ось чому ви отримаєте різні результати для різних компіляторів.
Кришна Кант Йенумула


-4

На практиці ви використовуєте невизначену поведінку. Може трапитися все, що завгодно, не тільки те, що ви вважаєте "розумним", і часто таке відбувається трапляються які ви не вважаєте розумними. Все за визначенням "розумно".

Дуже розумна компіляція полягає в тому, що компілятор зауважує, що при виконанні оператора буде викликано невизначену поведінку, тому оператор ніколи не може бути виконаний, тому він перекладається в інструкцію, яка навмисно збиває вашу програму. Це дуже розумно. Зрештою, компілятор знає, що цей збій ніколи не може статися.


1
Я вважаю, що ви неправильно зрозуміли питання. Питання стосується загальної чи специфічної поведінки, яка може призвести до конкретних результатів (результати x = 4, 5 або 6). Якщо вам не подобається моє вживання слова "розумний", я спрямовую вас на мій коментар вище, на який ви відповіли: "Використання" розумного "полягало лише у тому, щоб уникнути будь-кого, щоб сказати" компілятор міг зробити ЩО-небудь, навіть видати єдину інструкцію "" встановити х на 7. "" "Якщо у вас є кращі формулювання для питання, яке зберігає загальну ідею, я відкритий до нього. Крім того, схоже, ви повторно розмістили свою відповідь.
кориця

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