Філософія невизначеної поведінки


59

Специфікації C \ C ++ не дозволяють компіляторам відкрити велику кількість форм поведінки, щоб реалізувати їх по-своєму. Існує ряд питань, які завжди запитують тут про те саме, і у нас є чудові повідомлення про це:

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

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

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

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


7
хто все-таки чув про детермінований комп'ютер?
sova

1
як вказує відмінна відповідь програмістів litb.stackexchange.com/a/99741/192238 , заголовок та суть цього питання здаються дещо невідповідними: "поведінка, відкрита для компіляторів, щоб реалізувати по-своєму", зазвичай називають визначеною реалізацією . звичайно, власне UB дозволяється визначати автором впровадження, але частіше за все вони не турбують (і оптимізують все це, і т. д.)
underscore_d

Щось подібне до цього softwareengineering.stackexchange.com/questions/398703/…
Sisir

Відповіді:


49

По-перше, зазначу, що хоча я тут згадую лише "C", те ж саме стосується C ++.

Коментар, що згадував Годеля, був частково (але лише частково) суттєвим.

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

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

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

Загрожуючи звинуватити в зарозумілості (ще раз), я б охарактеризував стандарт С таким, що керується (частково) двома основними ідеями:

  1. Мова повинна підтримувати якомога більше різноманітних апаратних засобів (в ідеалі, все "розумне" обладнання до нижньої межі).
  2. Мова повинна підтримувати написання якомога більшого різноманіття програмного забезпечення для даного середовища.

Перший означає, що якщо хтось визначає новий процесор, для цього слід забезпечити хорошу, міцну, зручну реалізацію C, якщо дизайн принаймні розумно наближається до кількох простих рекомендацій - в основному, якщо це дотримується чогось із загального порядку моделі Фон Неймана і забезпечує принаймні якийсь розумний мінімальний об'єм пам'яті, якого повинно бути достатньо, щоб дозволити реалізацію C. Для "розміщеної" реалізації (тієї, яка працює на ОС) вам потрібно підтримати певне поняття, яке досить близько відповідає файлам, і мати набір символів з певним мінімальним набором символів (потрібно 91).

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

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

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

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

Ці відмінності у намірах сприяють більшості відмінностей між C та чимось на кшталт Java або систем на базі CLI на базі Microsoft. Останні досить чітко обмежені роботою зі значно обмеженим набором апаратних засобів або потребують програмного забезпечення для емуляції більш конкретного обладнання, на яке вони націлені. Вони також спеціально мають намір не допустити будь-яких прямих маніпуляцій з обладнанням, замість цього вимагають використовувати щось на зразок JNI або P / Invoke (і код, записаний у чомусь на зразок C), щоб навіть зробити таку спробу.

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


25
Я думаю, що теореми Годеля - це червона оселедець. Вони мають справу з доведенням системи з власних аксіом, що тут не так: C не потрібно вказувати в C. Цілком можливо мати повністю вказану мову (розглянемо машину Тюрінга).
poolie

9
Вибачте, але я боюся, що ви повністю зрозуміли теореми Годеля. Вони мають справу з неможливістю довести всі правдиві твердження у послідовній логічній системі; з точки зору обчислень, теорема про незавершеність є аналогічною тому, що існують проблеми, які неможливо вирішити жодною програмою - проблеми аналогічні істинним твердженням, програми доведенням і модель обчислення логічної системи. Він взагалі не має зв'язку з невизначеною поведінкою. Пояснення щодо аналогії див. Тут: scottaaronson.com/blog/?p=710 .
Алекс десять Бринк

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

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

20

C обгрунтування пояснює

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

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

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

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

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

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

І на 1.7 це зазначає

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

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

Таким чином, ця маленька брудна програма, яка прекрасно працює на GCC, все ще відповідає !


15

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

Інша невизначена поведінка просто відображає реальність. Одним із прикладів є зміщення бітів із числом, більшим за тип. Це насправді відрізняється від апаратних поколінь однієї родини. Якщо у вас є 16-розрядний додаток, точний той самий двійковий файл дасть різні результати для 80286 і 80386. Отже, мовний стандарт говорить, що ми не знаємо!

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


+1 для другого абзацу, який показує те, що було б незручно вказати як поведінку, визначену реалізацією.
Девід Торнлі

3
Зсув бітів - лише приклад прийняття невизначеної поведінки компілятора та використання апаратних можливостей. Було б тривіально вказати результат С для невеликого зсуву, коли кількість більший за тип, але дорого реалізується на деяких апаратних засобах.
mattnz

7

Як один із прикладів, доступ до вказівника майже повинен бути визначеним, а не обов'язково лише з міркувань продуктивності. Наприклад, у деяких системах завантаження конкретних регістрів із вказівником генерує апаратне виключення. На SPARC доступ до неправильно вирівняного об’єкта пам'яті призведе до помилки шини, але на x86 це буде "просто" повільно. Складно вказати поведінку в тих випадках, оскільки базове обладнання диктує, що буде, а C ++ є портативним для багатьох типів обладнання.

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

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


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

6

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


1
Так? Які винятки стосуються вбудованого обладнання?
Мейсон Уілер

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

1
@Mason: Оскільки апаратне забезпечення має неправомірний доступ. Windows легко відкидає порушення доступу, і складніше для вбудованого обладнання без жодної операційної системи нічого не робити, окрім мертвих.
DeadMG

3
Також пам’ятайте, що не кожен процесор має MMU для захисту від неправомірного доступу до апаратних засобів. Якщо ви почнете вимагати, щоб ваша мова перевіряла всі вказівки доступу, вам доведеться емулявати MMU на процесорах без жодного - і таким чином ВСІЙ доступ до пам'яті стає надзвичайно дорогим.
пухнастий

4

C був винайдений на машині з 9-бітовими байтами і без одиниці з плаваючою точкою - припустимо, він мав намір байтів бути 9 бітами, слова 18 біт і що поплавці повинні бути реалізовані за допомогою попереднього AEEE754 аритматичного?


5
Я підозрюю, що ви думаєте про Unix - C спочатку використовувався на PDP-11, що було насправді досить звичайними чинними стандартами. Я думаю, що основна ідея все-таки стоїть.
Джеррі Труну

@Jerry - так, ти маєш рацію - я старію!
Мартін Бекетт

Так, боюся з кращими з нас.
Джері Коффін

4

Я не думаю, що першим обґрунтуванням для UB було надання компілятору місця для оптимізації, а просто можливість використовувати очевидну реалізацію для цілей у той час, коли архітектури мали більше різноманітності, ніж зараз (пам’ятайте, чи C розроблявся на PDP-11, який має дещо звичну архітектуру, перший порт був Honeywell 635, який набагато менш знайомий - адресований словом, використовуючи 36 бітових слів, 6 або 9 біт байт, 18 біт адреси ... ну, принаймні, він використовував 2 доповнення). Але якщо велика оптимізація не була ціллю, очевидна реалізація не включає додавання перевірок часу на переповнення, підрахунку зсуву за розміром регістра, псевдоніми у виразах, що змінюють кілька значень.

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


Цікаво, що спричинило зміну філософії UB від "Дозволити програмістам використовувати поведінку, піддану їхній платформі", до "Знайти виправдання, щоб дозволити компіляторам реалізувати абсолютно нерозумну поведінку"? Мені також цікаво, наскільки такі оптимізації в кінцевому підсумку покращують розмір коду після зміни коду для роботи під новим компілятором? Я не був би здивований, якщо в багатьох випадках єдиний ефект додавання таких "оптимізацій" до компілятора полягає в тому, щоб змусити програмістів писати більший і повільніший код, щоб уникнути того, щоб компілятор порушив його.
supercat

Це дрейф у POV. Люди стали менш усвідомлювати машину, на якій працює їх програма, вони стали більше перейматися переносимістю, тому уникали залежно від визначеної, не визначеної та визначеної поведінкою поведінки. Був тиск на оптимізаторів, щоб отримати найкращі результати на еталоні, а це означає використовувати будь-яку поблажливість, залишену мовою мов. Існує також той факт, що Інтернет - Usenet в той час, як нині - SE, - юристи з мови також схильні давати необ’єктивне уявлення про основні обґрунтування та поведінку авторів-укладачів.
AProgrammer

1
Цікавим мені є те, що я бачив, що «С вважає, що програмісти ніколи не будуть брати участь у невизначеній поведінці» - факт, який історично не був правдою. Правильне твердження було б "C передбачає, що програмісти не будуть викликати поведінку, визначену стандартом, якщо вони не будуть готові боротися з природними наслідками платформи такої поведінки. Враховуючи, що C був розроблений як мова програмування систем, значна частина його призначення було дозволити програмістам робити конкретні для системи речі, не визначені мовним стандартом; думка, що вони ніколи цього не роблять, є абсурдною.
supercat

Програмістам добре докладати додаткових зусиль для забезпечення переносимості у випадках, коли різні платформи по суті роблять різні речі , але письменники-компілятори втрачають час для всіх, коли вони усувають поведінку, яку історики, які історично могли б вважати, були спільними для всіх майбутніх компіляторів. З огляду на цілі числа iі nтакі , що n < INT_BITSі i*(1<<n)НЕ буде переповнюватися, я вважав би , i<<=n;щоб ясніше i=(unsigned)i << n;; на багатьох платформах це було б швидше і менше, ніж i*=(1<<N);. Що отримують, забороняючи компілятори?
supercat

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

3

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


Ціле доповнення - цікавий випадок; окрім можливості поведінки пастки, яка в деяких випадках була б корисною, але в інших випадках може спричинити виконання випадкового коду, існують ситуації, коли компілятору було б доцільно робити висновки, виходячи з того, що ціле переповнення не вказано для завершення. Наприклад, компілятор, у якому int16 біт і знаки з розширенням знаків дорогі, можна обчислити, (uchar1*uchar2) >> 4використовуючи зсув без розширення без знаків. На жаль, деякі компілятори поширюють умовиводи не лише до результатів, але й до операндів.
supercat

2

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


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

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

1

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


або ви можете виділити всі покажчики як weak_ptrі звести нанівець всі посилання на покажчик, який отримує deleted ... о зачекайте, ми підходимо до збору сміття: /
Matthieu M.

boost::weak_ptrРеалізація є досить хорошим шаблоном для початку для цієї схеми використання. Замість того, щоб відстежувати і зводити нанівець weak_ptrsзовні, weak_ptrсправедливий сприяє shared_ptrслабкому рахунку, а слабкий підрахунок - це в основному відмова від самого покажчика. Таким чином, ви можете звести нанівець shared_ptrбез необхідності видаляти його негайно. Це не ідеально (у вас все ще може бути безліч придатних weak_ptrтермінів із збереженням основи shared_countбез поважних причин), але принаймні це швидко та ефективно.
пухнастий

0

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

Це в основному дає вам два варіанти:

1) Скиньте все з реєстрів перед будь-яким доступом через покажчик, лише на випадок, якщо вказівник вказує на пам'ять певної змінної. Потім завантажте все необхідне назад в регістр, про всяк випадок, якщо значення були змінені через покажчик.

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

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


Існує семантична різниця між тим, що говорити "Компілятору дозволено поводитись так, ніби X правдиво", і сказати "Будь-яка програма, де X не відповідає дійсності, буде брати участь у невизначеному поведінці", хоча, на жаль, стандарти не дозволяють зробити це чітким. У багатьох ситуаціях, включаючи ваш примірник-приклад, попереднє твердження дозволило б зробити багато оптимізацій компілятора, інакше було б неможливо; остання дозволяє ще кілька "оптимізацій", але багато останніх оптимізацій - це те, що програмісти не хотіли б.
supercat

Наприклад, якщо деякий код встановлює fooзначення 42, а потім викликає метод, який використовує нелегітимно модифікований покажчик для встановлення fooна 44, я можу побачити користь, сказавши, що до наступного "законного" написання fooспроби його прочитати можуть законно урожай 42 або 44, а вираз подібний foo+fooміг би дати навіть 86, але я бачу набагато меншу користь від того, щоб дозволити компілятору робити розширені та навіть зворотно-активні умовиводи, змінюючи не визначене поведінку, чия правдоподібна "природна" поведінка була б доброякісною, на ліцензію генерувати безглуздий код.
supercat

0

Історично не визначене поведінка мало дві основні цілі:

  1. Щоб уникнути необхідності авторів компілятора генерувати код для обробки умов, які ніколи не мали відбуватися.

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

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

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

uint32_t foo(uint16_t q, int *p)
{
  if (q > 46340)
    *p++;
  return q*q;
}

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


-6

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


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