Чому x = x ++ не визначено?


19

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

Але чому?

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

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

Рекомендовані правила:

  1. Між точками послідовності не визначений порядок оцінювання.
  2. Побічні ефекти відбуваються негайно.

Немає жодної невизначеної поведінки. Вирази оцінюють до цього чи іншого значення, але, безумовно, не форматуватимуть ваш жорсткий диск (як не дивно, я ніколи не бачив реалізації, де x=x++формати жорсткого диска).

Приклади виразів

  1. x=x++- Добре визначений, не змінюється x.
    Спочатку xзбільшується (одразу, коли x++оцінюється), потім зберігається старе значення x.

  2. x++ + ++x- Збільшення xвдвічі, оцінюється до 2*x+2.
    Хоча будь-яка сторона може бути оцінена спочатку, результат є або x + (x+2)(перший лівий бік), або (x+1) + (x+1)(правий перший спочатку).

  3. x = x + (x=3)- Не xвизначено , встановлено x+3або 6.
    Якщо права сторона оцінюється спочатку, то це x+3. Можливо також, що x=3оцінюється спочатку, так що це 3+3. У будь-якому випадку x=3присвоєння відбувається негайно при x=3оцінці, тому збережене значення переписується іншим завданням.

  4. x+=(x=3)- Добре визначено, встановлюється x6.
    Ви можете стверджувати, що це лише стенограма для виразу вище.
    Але я б сказав, що він +=повинен бути виконаний після x=3, а не у двох частинах (читати x, оцінювати x=3, додавати та зберігати нове значення).

Яка перевага?

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

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

Дуже основне правило таке:
Якщо A дійсне, а B - дійсне, і вони об'єднані в дійсний спосіб, результат є дійсним.
xє дійсним значенням L, x++є дійсним виразом і =є коректним способом поєднання значення L та виразу, тож як x=x++це не законно?
Стандарт C робить тут виняток, і цей виняток ускладнює правила. Ви можете шукати stackoverflow.com і бачити, наскільки цей виняток бентежить людей.
Тому я кажу - позбудьтеся цієї плутанини.

=== Підсумок відповідей ===

  1. Навіщо це робити?
    Я намагався пояснити в розділі вище - я хочу, щоб правила C були простими.

  2. Потенціал для оптимізації:
    це вимагає деякої свободи від компілятора, але я не бачив нічого, що переконувало б мене, що це може бути значним.
    Більшість оптимізацій все-таки можна зробити. Наприклад, a=3;b=5;можна переупорядкувати, навіть якщо стандарт визначає порядок. Такі вирази, як і a=b[i++]раніше, можна оптимізувати аналогічно.

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


10
Чому це важливо для вас? Чи слід це визначати, і якщо так, то чому? Немає сенсу привласнювати xсебе, і якщо ви хочете збільшити, xви можете просто сказати x++;- у призначенні немає потреби. Я б сказав, що це не слід визначати лише тому, що важко буде згадати, що повинно відбутися.
Калеб

4
На мій погляд, це гарне запитання ("Деякі чоловіки бачать речі такими, якими вони є, і запитують, чому я мрію про речі, які ніколи не були, і запитую, чому ні"). Це (на мою думку) питання виключно щодо дизайну мови, використовуючи синтаксис C як приклад, а не питання про синтаксис C. Я особисто вважаю, що причина у нас не визначена поведінка для таких виразів, як x ++ + ++ x або x = x ++, є просто тому, що існує можливість їх неправильного прочитання.
Джеймі Тейлор

5
@ugoren: Чому потрібно передбачити результат. Ніхто з розуму не писатиме такий код (як це вже згадувалося кілька разів), навіть якщо ви писали такий код, він буде відхилений при першому огляді коду. Тому немає необхідності визначати поведінку та надавати оптимізатору найкращий шанс при її оптимізації. У кожному прикладі, який ви пропонуєте, я б застрелив когось, якби він додав це до бази коду.
Мартін Йорк

3
Мені було б цікавіше питання, чому це не помилка написання цього? Безумовно, компілятор міг би виявити його невизначене поводження, і тому, можливо, не може бути те, що користувач насправді хотів, тому чому не помилка? Я розумію, деякі випадки невизначеного бахавуру важко виявити, але це не так; t
JohnB

3
« Правило забороняє зміну змінної в два рази між точками послідовності, звичайно , правило більшість програмістів не розуміють. » - Чи є у вас якісь - які докази для цього? Більшість запитань, які я бачив, задавали мені, оскільки програмісти не знали про це правило. Чи є докази того, що більшість із них досі не розуміли цього після пояснення?
Безпечний

Відповіді:


24

Можливо, спершу слід відповісти на питання, чому це слід визначити? Чи є якась перевага в стилі програмування, читабельності, ремонтопридатності чи продуктивності, дозволяючи таким виразам із додатковими побічними ефектами? Є

y = x++ + ++x;

легше читати, ніж

y = 2*x + 2;
x += 2;

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


1
Я додав розділ "чому" до свого питання. Я точно не пропоную використовувати ці вирази, але мені цікаво мати прості правила, щоб сказати значення виразу.
ugoren

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

3
Ну і більш філософська відповідь: Наразі це не визначено. Якщо жоден програміст не використовує його, то вам не потрібно розуміти такі вирази, оскільки не повинно бути жодного коду. Якщо вам потрібно зрозуміти їх, тоді, очевидно, повинно бути багато коду, який покладається на невизначену поведінку. ;)
Безпечний

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

1
@ugoren: Ваш розділ "чому" все ще не відповідає практичному запитанню: чому б ви хотіли цього дивного вираження у вашому коді? Якщо ви не можете придумати переконливу відповідь на це, то вся дискусія суперечить.
Майк Баранчак

20

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

Коли C був новим, машини, які могли скористатися цим для кращої оптимізації, були здебільшого теоретичними моделями. Люди говорили про можливість побудови процесорів, де компілятор дав би вказівку процесору про те, які вказівки можна / слід виконувати паралельно з іншими інструкціями. Вони вказували на той факт, що дозволити цьому не визначене поведінку означає, що на такому процесорі, якщо він коли-небудь дійсно існував, ви можете запланувати частину інструкції, що «збільшується», паралельно з рештою потоку інструкцій. Незважаючи на те, що вони мали рацію щодо теорії, на той час мало що було на шляху апаратних засобів, які могли б реально скористатися цією можливістю.

Це вже не просто теоретично. Зараз у виробництві та у широкому використанні є апаратне забезпечення (наприклад, Itanium, VLIW DSP), яке може реально скористатися цим. Вони дійсно роблять дозволяють компілятору генерувати потік команд , який вказує , що інструкції X, Y і Z можуть бути виконані паралельно. Це вже не теоретична модель - це справжнє обладнання в реальному використанні, виконуючи реальну роботу.

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

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

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

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

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


ІМО, мало причин вважати, що в більшості випадків повільніші інструкції насправді набагато повільніші, ніж швидкі інструкції, і що вони завжди матимуть вплив на продуктивність програми. Я б класифікував цей під час передчасної оптимізації.
DeadMG

Можливо, мені чогось не вистачає - якщо ніхто ніколи не повинен писати такий код, навіщо дбати про його оптимізацію?
ugoren

1
@ugoren: писати код на зразок a=b[i++];(на один приклад) добре, і оптимізувати його - це добре. Я, однак, не бачу сенсу зашкодити розумному коду, так що щось на зразок ++i++має певне значення.
Джеррі Труну

2
@ugoren Проблема одна з діагнозів. Єдина мета - не прямо заборонити такі вирази, як ++i++саме те, що взагалі важко відрізнити їх від дійсних виразів із побічними ефектами (такими як a=b[i++]). Нам це може здатися досить простим, але якщо я правильно пам’ятаю Книгу Драконів, то це насправді проблема, яка є NP. Ось чому така поведінка є UB, а не заборонена.
Конрад Рудольф

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

9

Ерік Ліпперт, головний дизайнер команди компілятора C #, опублікував у своєму блозі статтю про цілий ряд міркувань, які входять у вибір, щоб зробити функцію невизначеною на рівні мовної специфікації. Очевидно, що C # - це інша мова, і в її мовному дизайні впливають різні фактори, але точки, які він робить, є актуальними.

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

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


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

5

Спочатку розглянемо визначення невизначеної поведінки:

3.4.3

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

2. ПРИМІТКА Можлива невизначена поведінка варіюється від ігнорування ситуації повністю з непередбачуваними результатами, до поведінки під час перекладу чи виконання програми в документований спосіб, характерний для оточуючого середовища (з видачею діагностичного повідомлення або без нього), до припинення перекладу чи виконання (з видачею діагностичного повідомлення).

3 ПРИКЛАД Прикладом невизначеної поведінки є поведінка на ціле число над львом

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

Основою обговорюваного питання є наступний пункт:

6.5 Вирази

...
3 Групування операторів і операндів позначається синтаксисом. 74) За винятком випадків , специфічна ред пізніше (для виклику функції (), &&, ||, ?:, і оператори коми), порядок обчислення подвираженій і порядок , в якому побічні ефекти мають місце обидва unspeci фі - е изд .

Акцент додано.

Дано вираз, як

x = a++ * --b / (c + ++d);

подвираженія a++, --b, cі ++dможуть бути оцінені в будь-якому порядку . Крім того, побічні ефекти a++, --bі ++dможуть бути застосовані в будь-який момент до наступної точки послідовності (IOW, навіть якщо a++оцінюються , перш ніж --b, це не гарантує , що aбуде оновлена , перш ніж --bоцінюються). Як уже говорили інші, обґрунтуванням такої поведінки є надання реалізації реалізації свободи перепорядковувати операції оптимально.

Через це, однак, вирази люблять

x = x++
y = i++ * i++
a[i] = i++
*p++ = -*p    // this one bit me just yesterday

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

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

Очевидно, ви можете спроектувати мову таким чином, щоб порядок оцінювання та порядок застосування побічних ефектів були чітко визначені, і Java, і C # роблять це, значною мірою для уникнення проблем, до яких призводять визначення C та C ++.

Отже, чому після 3 стандартних змін не було внесено зміни до C? Перш за все, у нас є застарілий код C 40 років, і це не гарантує, що така зміна не порушить цей код. Це покладає навантаження на авторів-компіляторів, оскільки така зміна негайно зробить усі існуючі компілятори невідповідними; всі повинні були б зробити значні переписування. І навіть на швидких, сучасних процесорах все-таки можна реалізувати реальні покращення продуктивності, налаштовуючи порядок оцінювання.


1
Дуже добре пояснення питання. Я не погоджуюся з порушенням застарілих додатків - спосіб реалізації невизначеної / не визначеної поведінки іноді змінюється між версією компілятора без будь-яких змін у стандарті. Я не пропоную змінити будь-яку певну поведінку.
ugoren

4

Спершу ви повинні зрозуміти, що не просто x = x ++ є невизначеним. Ніхто не піклується про x = x ++, оскільки незалежно від того, що ви його визначили, немає сенсу. Те, що не визначено, більше схоже на "a = b ++, де a і b стають однаковими", тобто

void f(int *a, int *b) {
    *a = (*b)++;
}
int i;
f(&i, &i);

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

load r1 = *b
copy r2 = r1
increment r1
store *b = r1
store *a = r2

або

load r1 = *b
store *a = r1
increment r1
store *b = r1

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


Ви дійсно показуєте випадок, коли моя пропозиція призводить до більшої роботи машини, але мені це здається незначним. І компілятор все ще має деяку свободу - єдина реальна вимога, яку я додаю, - це зберігати bраніше a.
ugoren

3

Спадщина

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

Звичайно, ви можете винайти нову мову, скажімо, C + = , зі своїми правилами. Але це не буде C.


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

2

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

Основна проблема припущення не з x = x++;(компілятори можуть легко перевірити це і повинні попередити), це з *p1 = (*p2)++еквівалентом ( p1[i] = p2[j]++;коли p1 і p2 - параметри функції), де компілятор не може легко знати, якщоp1 == p2 (у C99 restrictдодано, щоб розширити можливість припущення p1! = p2 між точками послідовності, тому вважалося, що можливості оптимізації важливі).


Я не бачу, як моя пропозиція щось змінює стосовно p1[i]=p2[j]++. Якщо компілятор може вважати, що його немає, немає жодних проблем. Якщо це не вдається, p2[j]спершу він повинен пройти збільшити книгу , зберігати p1[i]пізніше. За винятком втрачених можливостей для оптимізації, які не здаються значущими, я не бачу жодної проблеми.
ugoren

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

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

Проблема полягає не в тому, що потрібно змінювати компілятори про будь-які зміни мови, які потребують, це те, що зміни є повсюдними і їх важко знайти. Найбільш практичним підходом, мабуть, буде зміна проміжного формату, на якому працює оптимізатор, тобто роблячи вигляд, що x = x++;не написано, але t = x; x++; x = t;або x=x; x++;або що ви хочете як семантичний (а як щодо діагностики?). Для нової мови просто відмовтеся від побічних ефектів.
AProgrammer

Я не знаю занадто багато про структуру компілятора. Якби я дуже хотів змінити всі компілятори, я б більше піклувався. Але, можливо, трактування x++як точки послідовності, як ніби це виклик функції inc_and_return_old(&x)зробить трюк.
ugoren

-1

У деяких випадках цей вид коду був визначений в новій C ++ 11 Стандарту.


5
Хочете допрацювати?
ugoren

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