Чому f (i = -1, i = -1) невизначена поведінка?


267

Я читав про порядок порушень оцінювання , і вони дають приклад, який мене спантеличує.

1) Якщо побічний ефект на скалярний об'єкт не є послідовним щодо іншого побічного ефекту на той же скалярний об'єкт, поведінка не визначена.

// snip
f(i = -1, i = -1); // undefined behavior

У цьому контексті iє скалярний об’єкт , який, мабуть, означає

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

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

Може хтось, будь ласка, уточнить?


ОНОВЛЕННЯ

Я дуже ціную всю дискусію. Поки мені дуже подобається відповідь @ harmic, оскільки вона розкриває підводні камені та тонкощі визначення цього твердження, незважаючи на те, як прямо на перший погляд це виглядає. @ acheong87 вказує на деякі проблеми, які виникають при використанні посилань, але я думаю, що це ортогонально щодо аспекту побічних ефектів цього питання.


ПІДСУМОК

Оскільки це питання привернуло багато уваги, я підсуму основні моменти / відповіді. По-перше, дозвольте мені невеликий відступ, щоб зазначити, що "чому" може мати тісно пов'язані, але тонко різні значення, а саме "з якої причини ", "з якої причини " та "з якою метою ". Я згрупую відповіді, за яким із тих значень «чому» вони зверталися.

з якої причини

Основна відповідь тут приходить від Пола Дрейпера , причому Мартін Дж надав аналогічну, але не настільки велику відповідь. Відповідь Пола Дрейпера зводиться до

Це невизначена поведінка, оскільки не визначено, що таке поведінка.

Відповідь загалом дуже хороша з точки зору пояснення того, що говорить стандарт C ++. Він також розглядає деякі пов'язані випадки UB, такі як f(++i, ++i);і f(i=1, i=-1);. У першому із суміжних випадків незрозуміло, чи повинен бути перший аргумент, i+1а другий i+2чи навпаки; по-друге, не зрозуміло, чи iповинно бути 1 або -1 після виклику функції. Обидва ці випадки є UB, оскільки вони підпадають під таке правило:

Якщо побічний ефект на скалярний об’єкт не є наслідком щодо іншого побічного ефекту на той же скалярний об’єкт, поведінка не визначена.

Тому f(i=-1, i=-1)є також UB, оскільки він підпадає під одне і те ж правило, незважаючи на те, що наміри програміста (ІМХО) очевидні та однозначні.

Пол Дрейпер також чітко робить у своєму висновку, що

Чи могла бути визначена поведінка? Так. Це було визначено? Немає.

що підводить нас до питання "з якої причини / мети f(i=-1, i=-1)залишилась невизначена поведінка?"

з якої причини / мети

Хоча в стандарті C ++ є деякі пропуски (можливо, необережні), багато упущень є обґрунтованими та служать певній меті. Хоча я усвідомлюю, що метою часто є «полегшити роботу автора-компілятора», або «швидший код», в основному мені було цікаво дізнатись, чи є вагома причина відпустки f(i=-1, i=-1) як UB.

harmic і Supercat забезпечують основні відповіді , які забезпечують причину для UB. Хармік вказує, що оптимізуючий компілятор, який може розбити операції з нібито атомного присвоєння на декілька машинних інструкцій, і що він може додатково переплутати ці інструкції для досягнення оптимальної швидкості. Це може призвести до дуже дивних результатів: iза його сценарієм дорівнює -2! Таким чином, шкідливість демонструє, як присвоєння одній і тій же величині змінній більше одного разу може мати негативні наслідки, якщо операції не є наслідком.

supercat надає пов'язану експозицію про підводні камені спроб змусити f(i=-1, i=-1)зробити те, що схоже на те, що треба робити. Він вказує, що в деяких архітектурах існують жорсткі обмеження щодо декількох одночасних записів на одну і ту ж адресу пам'яті. Укладач міг би важко це зрозуміти, якби ми мали справу з чимось менш тривіальним, ніж f(i=-1, i=-1).

davidf також надає приклад інструкцій щодо переплетення, дуже схожих на шкідливі.

Хоча кожен із прикладів шкідливих, суперкотячих та давидфів дещо надуманий, узяте разом, вони все ще слугують чіткою причиною, чому f(i=-1, i=-1)слід не визначати поведінку.

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

інші відповіді

JohnB зазначає, що якщо ми розглянемо перевантажені оператори присвоєння (а не просто звичайні скаляри), то ми також можемо зіткнутися з проблемою.


1
Скалярний об’єкт - це об’єкт скалярного типу. Див. 3.9 / 9: "Арифметичні типи (3.9.1), типи перерахування, типи вказівників, вказівники на типи членів (3.9.2) std::nullptr_tта версії цих типів, кваліфіковані cv (3.9.3), називаються у сукупності скалярними типами . "
Роб Кеннеді

1
Можливо, на сторінці є помилка, і вони насправді мали на увазі f(i-1, i = -1)чи щось подібне.
Містер Лістер

Погляньте на це питання: stackoverflow.com/a/4177063/71074
Роберт С. Барнс

@RobKennedy Дякую Чи включають "арифметичні типи" bool?
Nicu Stiurca

1
SchighSchagh ваше оновлення має бути у розділі відповідей.
Grijesh Chauhan

Відповіді:


343

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

Якщо A не секвенується перед B, а B не є послідовним перед A, то існують дві можливості:

  • Оцінки A і B не мають наслідків: вони можуть виконуватися в будь-якому порядку і можуть перекриватися (у межах одного потоку виконання компілятор може переплутати інструкції CPU, що містять A і B)

  • Оцінки A і B є невизначено послідовними: вони можуть бути виконані в будь-якому порядку, але можуть не перетинатися: або A буде завершено до B, або B буде завершено до A. Порядок може бути протилежним наступного разу тим самим виразом оцінюється.

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

Наприклад, уявіть, що було ефективніше нульову пам'ять, а потім її зменшення порівняно з завантаженням значення -1 дюйма. Тоді це:

f(i=-1, i=-1)

можуть стати:

clear i
clear i
decr i
decr i

Зараз мені -2.

Це, мабуть, хибний приклад, але це можливо.


59
Дуже приємний приклад того, як вираз насправді може зробити щось несподіване, дотримуючись правил послідовності. Так, трохи надумано, але в першу чергу запитується код, про який я питаю. :)
Nicu Stiurca

10
І навіть якщо призначення виконується як атомна операція, можливо створити надскалярну архітектуру, де обидва призначення виконані одночасно, викликаючи конфлікт доступу до пам'яті, що призводить до збою. Мова розроблена так, щоб автори-компілятори мали якомога більше свободи у використанні переваг цільової машини.
ач

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

1
+ 1e + 6 (добре, +1) для точки, що складений код, не завжди є тим, що ви очікували. Оптимізатори дуже добре кидають на вас такі криві, коли ви не дотримуєтесь правил: P
Corey

3
На процесорі Arm навантаження 32-бітною може приймати до 4 інструкцій: це робить load 8bit immediate and shiftдо 4 разів. Зазвичай компілятор буде робити непряме звернення, щоб отримати число з таблиці, щоб уникнути цього. (-1 можна зробити за 1 інструкцію, але можна вибрати інший приклад).
ctrl-alt-delor

208

По-перше, "скалярний об'єкт" означає тип типу a int, floatабо покажчик (див. Що таке скалярний об'єкт у C ++? ).


По-друге, це може здатися більш очевидним

f(++i, ++i);

мав би невизначену поведінку. Але

f(i = -1, i = -1);

менш очевидний.

Трохи інший приклад:

int i;
f(i = 1, i = -1);
std::cout << i << "\n";

Яке призначення сталося "останнім" i = 1, або i = -1? Це не визначено в стандарті. Дійсно, це iможе бути 5(див. Шкідливу відповідь для цілком правдоподібного пояснення того, як це може бути). Або ви програму могли б за замовчуванням. Або переформатуйте жорсткий диск.

Але тепер ви запитуєте: "А як щодо мого прикладу? Я використовував однакове значення ( -1) для обох завдань. Що може бути незрозумілим щодо цього?"

Ви маєте рацію ... за винятком того, як це описав комітет зі стандартів C ++.

Якщо побічний ефект на скалярний об’єкт не є наслідком щодо іншого побічного ефекту на той же скалярний об’єкт, поведінка не визначена.

Вони могли зробити спеціальний виняток для вашого особливого випадку, але вони цього не зробили. (А навіщо їм? Яка користь від цього коли-небудь була б?) Отже, iмогла б бути 5. Або ваш жорсткий диск може бути порожнім. Таким чином, відповідь на ваше запитання:

Це невизначена поведінка, оскільки не визначено, що таке поведінка.

(Це заслуговує наголос, тому що багато програмістів вважають, що "невизначений" означає "випадковий" або "непередбачуваний". Це не так; це означає, що не визначено стандартом. Поведінка може бути на 100% послідовною і все ще не визначеною.)

Чи могла бути визначена поведінка? Так. Це було визначено? Ні. Отже, це "невизначено".

Однак це означає, що "невизначений" не означає, що компілятор буде форматувати ваш жорсткий диск ... це означає, що він міг би бути, і це все ще буде відповідати стандартам компілятору. Реально, я впевнений, що g ++, Clang та MSVC зроблять все, що ви очікували. Вони просто не повинні "робити".


Можливо, інше питання: Чому комітет зі стандартів C ++ вирішив зробити цей побічний ефект не наслідком? . Ця відповідь буде включати історію та думки комітету. Або Що добре, якщо цей побічний ефект не буде наслідком у С ++? , що дозволяє будь-яке обґрунтування, незалежно від того, чи це було фактичне обґрунтування комітету зі стандартів. Ви можете задати ці питання тут або на programmers.stackexchange.com.


9
@hvd, так, насправді я знаю, що якщо ви ввімкнете -Wsequence-pointg ++, він попередить вас.
Пол Дрейпер

47
"Я впевнений, що g ++, Clang та MSVC всі зроблять те, що ви очікували", я б не довіряв сучасному компілятору. Вони злі. Наприклад, вони можуть визнати, що це невизначена поведінка, і вважати, що цей код недоступний. Якщо вони цього не роблять сьогодні, вони можуть зробити це завтра. Будь-який UB - це бомба, що тикає.
CodesInChaos

8
@BlacklightShining "Ваша відповідь погана, тому що це не добре", не дуже корисні відгуки, чи не так?
Вінсент ван дер Веель

13
@BobJarvis Компілятор абсолютно не зобов'язаний генерувати навіть віддалено правильний код за умови не визначеної поведінки. TIt може навіть припустити, що цей код ніколи навіть не називається і таким чином замінює всю справу nop (зауважте, що компілятори насправді роблять такі припущення перед обличчям UB). Тому я б сказав, що правильна реакція на такий звіт про помилку може бути лише "закрита, працює за призначенням"
Grizzly

7
@SchighSchagh Іноді перефразовування термінів (які лише на поверхні виглядають як тавтологічна відповідь) - те, що потрібно людям. Більшість людей , які не знайомі з технічними характеристиками, думають , що undefined behaviorозначає something random will happen, що це далеко не в більшості випадків.
Ізката

27

Практична причина не робити виняток із правил лише тому, що два значення однакові:

// config.h
#define VALUEA  1

// defaults.h
#define VALUEB  1

// prog.cpp
f(i = VALUEA, i = VALUEB);

Розглянемо випадок, коли це було дозволено.

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

 #define VALUEB 2

Здається, нешкідливим, чи не так? І все-таки prog.cpp більше не збирається. Однак ми вважаємо, що компіляція не повинна залежати від значення букваря.

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

EDIT

@HeartWare вказував, що постійні вирази форми A DIV Bне дозволяються в деяких мовах, коли Bце 0, і викликають збій збірки. Отже, зміна константи може спричинити помилки компіляції в іншому місці. Що, ІМХО, прикро. Але, безумовно, добре обмежити такі речі неминучими.


Звичайно, але приклад робить використання цілого числа літералів. У f(i = VALUEA, i = VALUEB);вас, безумовно, потенціал для невизначеної поведінки. Сподіваюся, ви насправді не кодуєте значення, що стоять за ідентифікаторами.
Вовк

3
@Wold Але компілятор не бачить макросів препроцесора. І навіть якби це було не так, важко знайти приклад в будь-якій мові програмування, де вихідний код збирається, поки не зміниться інта-константа з 1 на 2. Це просто неприйнятно і незрозуміло, хоча тут ви бачите дуже хороші пояснення. чому цей код порушений навіть з однаковими значеннями.
Інго

Так, компіляції не бачать макросів. Але це було питання?
Вовк

1
У вашій відповіді відсутня суть, прочитайте шкідливу відповідь та коментар ОП до неї.
Вовк

1
Це могло зробити SomeProcedure(A, B, B DIV (2-A)). У будь-якому випадку, якщо мова зазначає, що CONST необхідно повністю оцінити під час компіляції, то, звичайно, моя заява не є дійсною для цього випадку. Оскільки це якимось чином розмиває відмінність компіляції та часу виконання. Чи це також помітить, якщо ми пишемо CONST C = X(2-A); FUNCTION X:INTEGER(CONST Y:INTEGER) = B/Y; ?? Або функції заборонені?
Інго

12

Плутанина полягає в тому, що зберігання постійного значення в локальній змінній - це не одна атомна інструкція для кожної архітектури, на якій C призначений для запуску. Код процесора працює в питаннях більше, ніж компілятор у цьому випадку. Наприклад, для ARM, де кожна інструкція не може нести повну константу 32 біта, для зберігання int у змінній потрібно більше, ніж одна інструкція. Приклад із цим псевдокодом, коли ви можете зберігати лише 8 біт одночасно і працювати в 32-бітовому реєстрі, я є int32:

reg = 0xFF; // first instruction
reg |= 0xFF00; // second
reg |= 0xFF0000; // third
reg |= 0xFF000000; // fourth
i = reg; // last

Ви можете уявити, що якщо компілятор хоче оптимізувати, він може переплутати одну і ту ж послідовність двічі, і ви не знаєте, яке значення буде записане в i; і скажемо, що він не дуже розумний:

reg = 0xFF;
reg |= 0xFF00;
reg |= 0xFF0000;
reg = 0xFF;
reg |= 0xFF000000;
i = reg; // writes 0xFF0000FF == -16776961
reg |= 0xFF00;
reg |= 0xFF0000;
reg |= 0xFF000000;
i = reg; // writes 0xFFFFFFFF == -1

Однак у моїх тестах gcc досить любезний, щоб визнати, що одне і те ж значення використовується двічі і генерує його один раз, і нічого дивного не робить. Я отримую -1, -1 Але мій приклад все ще справедливий, оскільки важливо враховувати, що навіть константа може бути не такою очевидною, як здається.


Я припускаю, що на ARM компілятор просто завантажить константу з таблиці. Те, що ви описуєте, схоже на MIPS.
ач

1
@AndreyChernyakhovskiy Так, але у випадку, коли це не просто -1(що компілятор десь зберігав), а швидше 3^81 mod 2^32, але постійний, тоді компілятор може робити саме те, що робиться тут, і в певному важелі omtimization мій переплутати послідовності викликів щоб не чекати.
йо

@tohecz, так, я це вже перевірив. Дійсно, компілятор занадто розумний, щоб завантажувати кожну константу з таблиці. У будь-якому разі, він ніколи не використовував би один і той же регістр для обчислення двох констант. Це так само точно би «визначило» визначену поведінку.
ач

@AndreyChernyakhovskiy Але ти, мабуть, не "кожен програміст компілятора C ++ у світі". Пам’ятайте, що є машини з 3 короткими регістрами, доступними лише для обчислень.
йо

@tohecz, розглянемо на прикладі, f(i = A, j = B)де iі jє два окремі об’єкти. У цьому прикладі немає UB. Машина, що має 3 короткі регістри, не є приводом для компілятора змішувати два значення Aта Bв одному реєстрі (як це показано у відповіді @ davidf), оскільки це порушило б семантику програми.
ач

11

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

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

Звичайно, той факт, що хтось може реалізувати компілятор C на такій платформі, не говорить про те, що така поведінка не повинна визначатися на апаратних платформах при використанні магазинів типів, малих, щоб оброблятись атомно. Спроба зберігати два різних значення непідвладним способом може спричинити дивацтво, якщо компілятор цього не знає; Наприклад, дано:

uint8_t v;  // Global

void hey(uint8_t *p)
{
  moo(v=5, (*p)=6);
  zoo(v);
  zoo(v);
}

якщо компілятор вказує рядки виклику "moo" і може сказати, що він не змінює "v", він може зберігати від 5 до v, потім зберігати від 6 до * p, потім передавати 5 в "zoo", а потім передайте вміст v в «зоопарк». Якщо "zoo" не модифікує "v", не повинно бути способом передавання двох викликів різних значень, але це може легко відбутися в будь-якому випадку. З іншого боку, у випадках, коли обидва магазини писали б одне і те ж значення, така дивацтво не могло виникнути, і на більшості платформ не було б розумних причин для здійснення будь-якого дивного. На жаль, деяким письменникам-компіляторам не потрібно жодного виправдання для дурної поведінки за межами "тому, що стандарт дозволяє", тому навіть ці випадки не є безпечними.


9

Той факт, що результат у цій справі був би однаковим у більшості реалізацій, є випадковим; порядок оцінювання ще не визначений. Поміркуйте f(i = -1, i = -2): ось, замовляйте питання. Єдина причина, яка не має значення у вашому прикладі, - це випадковість, що є обома значеннями -1.

Зважаючи на те, що вираз визначений як один із невизначеною поведінкою, шкідливий сумісний компілятор може відображати невідповідне зображення, коли ви оцінюєте f(i = -1, i = -1)та скасовуєте виконання - і все ще вважатиметься абсолютно правильним. На щастя, жоден компілятор, про який я знаю, не робить цього.


8

Мені здається, що єдине правило, яке стосується послідовності вираження аргументу функції, знаходиться тут:

3) При виклику функції (незалежно від того, функція вбудована чи ні, чи використовується явний синтаксис виклику функції), кожне обчислення значення та побічний ефект, пов'язаний з будь-яким виразом аргументу, або з виразом postfix, що позначає викликану функцію, є секвенсовані перед виконанням кожного виразу або висловлювання в тілі викликаної функції.

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

1) Якщо побічний ефект на скалярний об’єкт не є наслідком щодо іншого побічного ефекту на той же скалярний об’єкт, поведінка не визначена.

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

void f(int l, int r) {
    return l < -1;
}
auto b = f(i = -1, i = -2);
if (b) {
    formatDisk();
}

8

C ++ 17 визначає суворіші правила оцінки. Зокрема, це послідовності функцій аргументів (хоча й у не визначеному порядку).

N5659 §4.6:15
Оцінки A і B є невизначено послідовними, коли або A секвенсовані перед B, або B секвенсовані перед A , але не визначено, який. [ Примітка : Оцінки з невизначеною послідовністю не можуть перетинатися, але вони можуть бути виконані спочатку. - кінцева примітка ]

N5659 § 8.2.2:5
Ініціалізація параметра, включаючи всі пов'язані з ним обчислення значень та побічний ефект, невизначено секвенується щодо будь-якого іншого параметра.

Це дозволяє деякі випадки, які були б раніше:

f(i = -1, i = -1); // value of i is -1
f(i = -1, i = -2); // value of i is either -1 or -2, but not specified which one

2
Дякуємо, що додали це оновлення для c ++ 17 , тому мені не довелося. ;)
Якк - Адам Невраумон

Дивовижний, велике спасибі за цю відповідь. Невелике подання: якщо fпідпис був f(int a, int b), чи гарантує C ++ 17 a == -1і b == -2якщо його викликають, як у другому випадку?
Nicu Stiurca

Так. Якщо у нас є параметри aі b, то або i-then- aинициализируется -1, після чого i-then- bинициализируется до -2, або навпаки. В обох випадках ми закінчуємо a == -1і b == -2. Принаймні, так я читаю: " Ініціалізація параметра, включаючи всі пов'язані з ним обчислення значень та побічний ефект, невизначено секвенсована стосовно до будь-якого іншого параметра ".
AlexD

Я думаю, що це було одне і те ж у С від назавжди.
фуз

5

Оператор призначення може бути перевантажений, і в цьому випадку порядок може мати значення:

struct A {
    bool first;
    A () : first (false) {
    }
    const A & operator = (int i) {
        first = !first;
        return * this;
    }
};

void f (A a1, A a2) {
    // ...
}


// ...
A i;
f (i = -1, i = -1);   // the argument evaluated first has ax.first == true

1
Щоправда, але питання стосувалося скалярних типів , на які інші вказували, значить, по суті, сім'я, плаваюча сім'я та покажчики.
Nicu Stiurca

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

2

Це просто відповідь на "Я не впевнений, що може означати" скалярний об'єкт ", окрім чогось на зразок int чи float".

Я б інтерпретував "скалярний об'єкт" як абревіатуру "об'єкт скалярного типу", або просто "змінну скалярного типу". Потім pointer, enum(константа) мають скалярного типу.

Це стаття MSDN " Скалярні типи" .


Це читається трохи як "відповідь лише на посилання". Чи можете ви скопіювати відповідні біти з цього посилання на цю відповідь (в блок-котировці)?
Коул Джонсон

1
@ColeJohnson Це не лише посилання на відповідь. Посилання лише для подальшого пояснення. Моя відповідь - "покажчик", "перерахунок".
Пен Чжан

Я не сказав, що ваша відповідь була лише посиланням. Я сказав, що "читається як [один]" . Я пропоную вам прочитати, чому ми не хочемо відповіді лише на посилання у розділі довідки. Причина, якщо Microsoft оновлює свої URL-адреси на своєму сайті, це посилання розривається.
Коул Джонсон

1

Насправді, є причина не залежати від того, що компілятор перевірить, iчи присвоєно однакове значення двічі, так що можна замінити його одним призначенням. Що робити, якщо у нас є деякі вирази?

void g(int a, int b, int c, int n) {
    int i;
    // hey, compiler has to prove Fermat's theorem now!
    f(i = 1, i = (ipow(a, n) + ipow(b, n) == ipow(c, n)));
}

1
Не потрібно , щоб довести теорему Ферма: просто призначити 1на i. Або обидва аргументи призначають, 1і це робить "правильну" річ, або аргументи присвоюють різні значення, і це невизначена поведінка, тому наш вибір все ж дозволений.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.