Чому у C ++ є "невизначена поведінка" (UB), а інші мови, такі як C # або Java, не мають?


50

Цей пост переповнення стеку містить досить вичерпний перелік ситуацій, коли специфікація мови C / C ++ заявляється як "невизначена поведінка". Однак я хочу зрозуміти, чому інші сучасні мови, такі як C # або Java, не мають поняття "невизначена поведінка". Це означає, що дизайнер компілятора може керувати всіма можливими сценаріями (C # і Java) чи ні (C і C ++)?




3
і все-таки ця публікація " ТАК" стосується невизначеної поведінки навіть у специфікації Java!
gbjbaanb

"Чому у C ++ є" не визначена поведінка "" На жаль, це, здається, одне з тих питань, на які важко відповісти об'єктивно, поза твердженням ", оскільки з причин X, Y та / або Z (все це може бути nullptr) немає хтось заважав визначити поведінку, написавши та / або прийнявши запропоновану специфікацію ". : c
code_dredd

Я б оскаржив передумови. Принаймні на C # є "небезпечний" код. Microsoft пише "У певному сенсі написання небезпечного коду дуже схоже на написання коду C в рамках програми C #", і наводить приклади причин, чому можна було б це зробити: для доступу до обладнання або ОС та для швидкості. Це те, для чого вигадали С (пекло, вони написали ОС на С!), Так що там у вас є.
Пітер - Відновіть Моніку

Відповіді:


72

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

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

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

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

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

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


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

13
Основна частина відповіді для мене насправді не звучить переконливо. Я маю на увазі, що в основному неможливо написати функцію, яка безпечно додає два числа (як у int32_t add(int32_t x, int32_t y)) у C ++. Звичайні аргументи навколо цього пов'язані з ефективністю, але часто змішуються з деякими аргументами переносимості (як у "Пишіть один раз, запустіть ... на платформі, де ви це написали ... і ніде більше ;-)"). Приблизно, один аргумент може бути таким: деякі речі не визначені, оскільки ви не знаєте, чи перебуваєте ви на 16-бітовому мікроконтролері чи на 64-бітовому сервері (слабкий, але все-таки аргумент)
Marco13,

12
@ Marco13 Погодився - і позбутися проблеми "невизначеної поведінки", зробивши щось "визначене поведінку, але не обов'язково те, що користувач хотів і без попередження, коли це відбувається" замість "невизначеної поведінки" - це просто грати в коди-адвокати ІМО .
alephzero

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

34
"Невизначена поведінка - одна з тих речей, які були визнані дуже поганою ідеєю лише в ретроспективі". Це ваша думка. Багато хто (включаючи і мене) не поділяють це.
Гонки легкості з Монікою

103

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

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

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


19
Відмінна презентація дилеми HLL: безпека та простота використання та ефективність. Срібної кулі немає: для кожної сторони є випадки використання.
Крістоф

5
@Christophe Чесно кажучи, є набагато кращі підходи до проблеми, ніж відпускати UB абсолютно безперервно, як C і C ++. Ви можете мати безпечну, керовану мову, із втеченими люками на небезпечну територію, щоб вам застосувати там, де вигідно. TBH, було б дуже приємно просто скласти мою програму C / C ++ з прапором, який говорить: "Вставте будь-яку дорогу машину виконання, яка вам потрібна, мені все одно, але просто розкажіть про ВСЕ, що відбувається . "
Олександр

4
Хорошим прикладом структури даних, яка свідомо читає неініціалізовані місця, є розрізне представлення Бріггса та Торкзона (наприклад, див. Codingplayground.blogspot.com/2009/03/… ) Ініціалізацією такого набору є O (1) в C, але O ( n) з примусовою ініціалізацією Java.
Арка Д. Робісон

9
Незважаючи на те, що примусове ініціалізація даних робить порушені програми набагато передбачуванішими, це не гарантує наміченої поведінки: Якщо алгоритм очікує, що він буде читати змістовні дані, помилково читаючи неявно ініціалізований нуль, це стільки ж помилки, як якщо б він мав читати сміття. З програмою C / C ++ така помилка була б помітна, запустивши процес, під valgrindяким було б точно вказано, де було використано неініціалізоване значення. Ви не можете використовувати valgrindкод Java, оскільки час виконання ініціалізації робить valgrinds перевірки марними.
cmaster

5
@cmaster Ось чому компілятор C # не дозволяє читати з неініціалізованих місцевих жителів. Не потрібно перевіряти час виконання, немає необхідності в ініціалізації, просто аналіз часу компіляції. Це все ще є компромісом - є деякі випадки, коли у вас немає хорошого способу впоратися з розгалуженням навколо потенційно непризначених місцевих жителів. На практиці я не знайшов жодних випадків, коли б це було не в першу чергу поганим дизайном і було б краще вирішено шляхом переосмислення коду, щоб уникнути складного розгалуження (яке людям важко проаналізувати), але це принаймні можливо.
Луань

42

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

Дивіться http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html

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


Переповнене ціле число переповнення: Якщо арифметика типу "int" (наприклад) переповнюється, результат не визначений. Одним із прикладів є те, що "INT_MAX + 1" не гарантується як INT_MIN. Така поведінка дозволяє певні класи оптимізацій, важливі для деякого коду. Наприклад, знання того, що INT_MAX + 1 не визначено, дозволяє оптимізувати "X + 1> X" до "true". Знання множення "не може" переповнювати (тому що це буде невизначено) дозволяє оптимізувати "X * 2/2" до "X". Хоча це може здатися тривіальним, такі речі зазвичай піддаються вбудованому вбудованому макросу і розширенню макросів. Більш важлива оптимізація, що це дозволяє, - це петлі "<=", як це:

for (i = 0; i <= N; ++i) { ... }

У цьому циклі компілятор може припустити, що цикл буде повторювати рівно N + 1 раз, якщо значення "i" не визначено при переповненні, що дає змогу розпочати широкий діапазон оптимізацій циклу. З іншого боку, якщо змінна визначена для завершити переповнення, тоді компілятор повинен припустити, що цикл, можливо, нескінченний (що відбувається, якщо N - INT_MAX) - що потім вимикає ці важливі оптимізації циклу. Особливо це стосується 64-бітних платформ, оскільки стільки код використовує "int", як індукційні змінні.


27
Звичайно, справжня причина, чому переповнення підписаних цілих чисел не визначено, полягає в тому, що при розробці C існували щонайменше три різні представлення підписаних цілих чисел у використанні (одне доповнення, два доповнення, величина знаку і, можливо, зміщення двійкових) , і кожен дає різний результат для INT_MAX + 1. Здійснення невизначеного переповнення дозволяє a + bкомпілювати в початковій add b aінструкції в будь-якій ситуації, а не потенційно вимагати від компілятора імітувати якусь іншу форму підписаної цілої арифметики.
Марк

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

5
@supercat Ще одна причина, чому уникнення невизначеної поведінки частіше зустрічається в останніх мовах - час програміста цінується набагато більше, ніж час процесора. Такий тип оптимізацій, на який дозволено робити завдяки UB, на сучасних настільних комп’ютерах по суті є безглуздим, а міркування щодо коду набагато складніше (не кажучи вже про наслідки для безпеки). Навіть у критичному виконанні коду ви можете скористатися оптимізаціями на високому рівні, які було б дещо складніше (або навіть набагато складніше) зробити у C. У мене є власний програмний 3D-рендер у C #, і можливість використання, наприклад, HashSetє чудовим.
Луань

2
@supercat: Wrt_loosely определен_, логічним вибором для цілого переповнення буде вимагати поведінки, визначеної реалізацією . Це існуюча концепція, і це не надмірне навантаження на реалізацію. Підозрюю, що більшість з них піде з "доповненням 2". <<може бути важким випадком.
MSalters

@MSalters Існує просте і добре вивчене рішення, яке не є ні визначеною поведінкою, ні визначеною реалізацією поведінкою: недетермінованою поведінкою. Тобто, ви можете сказати, " x << yоцінює якесь дійсне значення типу, int32_tале ми не скажемо, яке". Це дозволяє реалізаторам використовувати швидке рішення, але не виступає помилковою передумовою, що дозволяє оптимізувати стиль подорожі в часі, оскільки недетермінізм обмежений результатом цієї однієї операції - специфікація гарантує, що пам'ять, мінливі змінні тощо не будуть помітно впливати за виразом оцінки. ...
Маріо Карнейро

20

У перші дні С було багато хаосу. Різні укладачі по-різному ставилися до мови. Коли з'явився інтерес написати специфікацію для цієї мови, ця специфікація повинна бути досить сумісною в порівнянні з C, на яку програмісти покладаються зі своїми компіляторами. Але деякі з цих деталей не є портативними і взагалі не мають сенсу, наприклад, якщо припустити певну цінність чи макет даних. Таким чином, стандарт C залишає багато деталей як невизначене або визначене для виконання поведінка, що залишає багато гнучкості для авторів-компіляторів. C ++ ґрунтується на C, а також відрізняється невизначеним поведінкою.

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


Так ви кажете, зворотна сумісність є єдиною причиною, чому C і C ++ не виходять із не визначеного поведінки?
Сісір

3
Це, безумовно, один із великих, @Sisir. Навіть серед досвідчених програмістів ви здивуєтеся, скільки речей, які не повинні ламати , ламаються, коли компілятор змінює спосіб обробки не визначеної поведінки. (Справа в тому, що було трохи хаосу, коли GCC почала оптимізувати " thisнуль?" Перевіряє деякий час назад, мотивуючи це thisтим, що nullptrє UB, і, таким чином, ніколи насправді не може статися.)
Justin Time 2 Reinstate Monica

9
@Sisir, ще одна велика - швидкість. У перші дні C обладнання було набагато більш неоднорідним, ніж сьогодні. Просто не вказуючи, що станеться, коли ви додасте 1 до INT_MAX, ви можете дозволити компілятору робити все, що є найшвидшим для архітектури (наприклад, система з доповненням однієї буде виробляти -INT_MAX, а система з двома доповненнями створюватиме INT_MIN). Аналогічно, не вказуючи, що відбувається, коли ви читаєте минулий кінець масиву, ви можете мати систему із захистом пам’яті, що припиняє програму, тоді як одній без неї не потрібно буде здійснювати дорогу перевірку меж виконання.
Марк

14

Мови JVM та .NET легко:

  1. Вони не повинні мати можливість безпосередньо працювати з обладнанням.
  2. Вони повинні працювати лише з сучасними настільними та серверними системами або з досить подібними пристроями, або принаймні пристроями, призначеними для них.
  3. Вони можуть накладати сміття для всієї пам'яті та примусової ініціалізації, отримуючи таким чином безпеку покажчиків.
  4. Їх уточнив один актор, який також забезпечив остаточну реалізацію.
  5. Вони можуть вибрати безпеку над продуктивністю.

Хоча для вибору є хороші моменти:

  1. Системне програмування - це зовсім інша кульова гра, і безкомпромісна оптимізація для програмування додатків є розумною.
  2. Правда, весь час є менш екзотичне обладнання, але тут залишаються невеликі вбудовані системи.
  3. GC погано підходить для негорючих ресурсів і торгує набагато більше місця для хорошої роботи. І більшість (але не майже всі) примусових ініціалізацій можна оптимізувати.
  4. Переваги є більшою конкуренцією, але комітети означають компроміс.
  5. Всі ці кордони провер дійсно складаються, хоча більшість з них може бути оптимізовано геть. Перевірка нульових покажчиків в основному може бути здійснена шляхом лову доступу до нуля накладних витрат завдяки віртуальному адресному простору, хоча оптимізація все ще гальмується.

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


3
Справді. Я програмую на C # для своєї роботи. Кожен раз за деякий час я добираюсь до одного з небезпечних молотків ( unsafeключового слова чи атрибутів у System.Runtime.InteropServices). Зберігаючи цей матеріал нечисленним програмістам, які знають, як налагодити некеровані речі, і знову ж таки мало того, що є практичним, ми залишаємо проблеми. Минуло більше 10 років з моменту останнього небезпечного молота, що стосується виступу, але іноді вам доведеться це робити, оскільки іншого рішення буквально немає.
Джошуа

19
Я часто працюю на платформі з аналогових пристроїв, де sizeof (char) == sizeof (короткий) == sizeof (int) == sizeof (float) == 1. Це також робить насичуючий додаток (так INT_MAX + 1 == INT_MAX) , і приємно про C - це те, що я можу мати відповідний компілятор, який генерує розумний код. Якщо мова, якою призначена мова, кажуть, що двійки доповнюються обертанням, то кожне доповнення закінчується тестом та гілкою, що є не початковою частиною, орієнтованою на DSP. Це поточна виробнича частина.
Ден Міллс

5
@BenVoigt Деякі з нас живуть у світі, де невеликий комп'ютер - це, можливо, 4 кб простору коду, фіксований 8-рівневий стек для виклику / повернення, 64 байти оперативної пам’яті, тактова частота 1 МГц і коштує <0,20 доларів у кількості 1000. Сучасний мобільний телефон - це невеликий ПК, який має необмежену кількість необмежених пам’яток для будь-яких намірів та цілей, і його можна з великим рахунком розглядати як ПК. Не весь світ є багатоядерним і не має жорстких обмежень у реальному часі.
Ден Міллз

2
@DanMills: Не кажучи про сучасні мобільні телефони з процесорами Arm Cortex A, а про "багатофункціональні телефони" близько 2002 року. Так, 192 КБ SRAM - це набагато більше 64 байт (що не "мало", але "крихітно"), але 192 Кб також не були точно названі "сучасним" робочим столом або сервером протягом 30 років. Також в ці дні 20 центів отримаєте MSP430 з набагато більше 64 байтами SRAM.
Бен Войгт

2
@BenVoigt 192kB може не бути робочим столом протягом останніх 30 років, але я можу запевнити вас, що цілком достатньо для розміщення веб-сторінок, що, я можу стверджувати, робить таку річ сервером за самим визначенням слова. Факт полягає в тому, що це цілком розумна (щедра, рівна) кількість оперативної пам’яті для багато вбудованих додатків, які часто включають веб-сервери конфігурації. Звичайно, я, мабуть, не запускаю на ньому амазонку, але я просто можу працювати на холодильнику в комплекті з IOT crapware на такому сердечнику (З часом та простором). Для цього нікому не потрібні тлумачні та JIT мови!
Ден Міллз

8

Java та C # характеризуються домінуючим постачальником, принаймні на початку свого розвитку. (Sun та Microsoft відповідно). C і C ++ різні; вони мали кілька конкуруючих реалізацій з самого початку. Особливо C працював і на екзотичних апаратних платформах. Як результат, між реалізаціями виникла різниця. Комітети ISO, які стандартизували C та C ++, могли домовитись про великий загальний знаменник, але в тих краях, де імплементація відрізняється, стандарти залишають місце для впровадження.

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


Що означає «великий загальний знаменник» буквально ? Ви говорите про підмножини чи суперсети? Чи дійсно ви маєте на увазі достатньо спільних факторів? Це схоже на найменш поширений кратний чи найбільший загальний фактор? Це дуже заплутано для нас роботів, які не говорять на вулиці, а лише на математику. :)
tchrist

@tchrist: Поширена поведінка є підмножиною, але ця підмножина є досить абстрактною. У багатьох областях, які не визначені загальним стандартом, реальні реалізації повинні зробити вибір. Зараз деякі з цих варіантів є досить зрозумілими, а тому визначені реалізацією, але інші є більш невиразними. Розмітки пам'яті під час виконання приклад: там повинен бути вибір, але це не ясно , як ви б документувати.
MSalters

2
Оригінальний C виготовив один хлопець. У дизайні вже було багато UB. Речі, звичайно, погіршилися, коли C став популярним, але UB був там з самого початку. Pascal and Smalltalk мали набагато менше UB і були розроблені приблизно в той же час. Основна перевага C полягала в тому, що надзвичайно просто перенести порт - всі проблеми переносимості були делеговані програмісту програми: P Я навіть переніс простий компілятор C у свій (віртуальний) процесор; робити щось на кшталт LISP або Smalltalk було б набагато більше зусиль (хоча у мене був обмежений прототип для .NET часу виконання :).
Луань

@Luaan: Це Керніган чи Річі? І ні, у неї не було визначеної поведінки. Я знаю, в мене на столі була оригінальна документація на компілятор AT&T. Реалізація зробила те, що зробила. Не було різниці між не визначеною та невизначеною поведінкою.
MSalters

4
@MSalters Річі був першим хлопцем. Керніган приєдналася (не набагато) пізніше. Що ж, у нього не було "Невизначеної поведінки", оскільки цей термін ще не існував. Але вона мала таку саму поведінку, яку сьогодні можна назвати невизначеною. Оскільки у C не було специфікації, навіть "невизначений" - це розтягнення :) Це компілятор не хвилювався, і деталі залежали від програмістів додатків. Він не був розроблений для створення портативних програм , лише компілятор повинен був легко переноситися.
Луань

6

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

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

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

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

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

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

Я бачу кілька можливостей, які залишає:

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

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

Ще один момент, який виникає досить часто - це розміри цілих чисел. C займає "положення", яке intмає бути природним розміром, запропонованим архітектурою. Отже, якщо я програмую 32-бітний VAX, intмабуть, це буде 32 біти, але якщо я програмую 36-бітний Univac, intмабуть, це буде 36 біт (і так далі). Мабуть, не розумно (а це навіть не можливо) писати операційну систему для 36-бітного комп'ютера, використовуючи лише типи, які гарантовано мають кратні розміри 8 біт. Можливо, я просто поверхневий, але мені здається, що якби я писав ОС для 36-бітної машини, я, мабуть, хотів би використовувати мову, яка підтримує 36-бітний тип.

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

Java та C # можуть ігнорувати все це. Вони не призначені для підтримки операційних систем запису. З ними у вас є пара варіантів. Одне полягає в тому, щоб апаратне забезпечення підтримувало те, чого вони вимагають - оскільки вони вимагають типів 8, 16, 32 та 64 біт, просто будуйте апаратне забезпечення, яке підтримує ці розміри. Інша очевидна можливість полягає в тому, щоб мова працювала лише над іншим програмним забезпеченням, яке забезпечує навколишнє середовище, яке вони хочуть, незалежно від того, що може хотіти базове обладнання.

У більшості випадків це насправді не вибір або вибір. Швидше за все, багато реалізацій трохи роблять і те, і інше. Ви зазвичай запускаєте Java на JVM, що працює в операційній системі. Найчастіше ОС записується на С, а JVM - на C ++. Якщо JVM працює на процесорі ARM, дуже ймовірно, що процесор включає в себе розширення Jazelle ARM для більш детального адаптації обладнання до потреб Java, тому менше потрібно робити програмне забезпечення, а код Java працює швидше (або менше) повільно, все одно).

Підсумок

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


4

Автори стандарту C очікували, що читачі визнають те, що вони вважають очевидним, і на це натякали у опублікованому Обґрунтуванні, але не сказали прямо: Комітету не потрібно було замовляти авторів-компіляторів для задоволення потреб своїх клієнтів, оскільки клієнти повинні краще, ніж Комітет, знати, які їх потреби. Якщо очевидно, що очікується, що компілятори для певних видів платформ оброблять конструкцію певним чином, нікому не слід хвилюватись, чи говорить Стандарт, що конструкція викликає не визначене поведінку. Невдача стандарту наказувати, що відповідні компілятори корисно обробляють фрагмент коду, жодним чином не означає, що програмісти повинні бути готові купувати компілятори, які цього не роблять.

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


Я відчуваю, що ви тут описали щось важливе, але це мені уникає. Чи можете ви пояснити свою відповідь? Особливо другий абзац: в ньому сказано, що умови зараз і умови раніше різні, але я цього не розумію; що саме змінилося? Також "шлях" зараз відрізняється, ніж раніше; можливо, поясніть це теж?
anatolyg

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

1
@anatolyg: Якщо ви ще цього не зробили, прочитайте опублікований документ C Обгрунтування (введіть C99 Обгрунтування в Google). Page 11 рядки 23-29 говорять про "ринок", а на сторінці 13 рядки 5-8 розповідають про те, що планується щодо портативності. Як ви думаєте, як реагує начальник комерційної компіляторської компанії, якби автор-компілятор сказав програмістам, які скаржилися, що оптимізатор порушив код, що кожен інший компілятор корисно поводився з тим, що їх код "зламаний", оскільки він виконує дії, не визначені Стандартом, і відмовився підтримувати це, оскільки це сприяло б продовженню ...
supercat

1
... використання таких конструкцій? Така точка зору легко очевидна на дошках підтримки clang і gcc і слугувала перешкодою для розвитку внутрішніх текстів, які могли б полегшити оптимізацію набагато легше і безпечніше, ніж зламані мови, які gcc і clang хочуть підтримувати.
supercat

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

3

У обох C ++ і c є описові стандарти (у будь-якому разі версії ISO).

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

Java та C # (або Visual C #, на який я маю на увазі) мають приписи . Вони розповідають, що в мові остаточно випереджає час, як це працює та що вважається дозволеною поведінкою.

Більш важливо, ніж це, Java насправді має "опорну реалізацію" у Open-JDK. (Я думаю, що Рослін вважається реалізацією посилань Visual C #, але не зміг знайти джерело для цього.)

У випадку Java, якщо в стандарті є якась неоднозначність, а Open-JDK робить це певним чином. Те, як це робить Open-JDK - це стандарт.


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

1

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

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

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

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

Ще коли було новим, ці міркування були важливими. Комп'ютери були менш потужними, часто мали обмежену швидкість обробки та пам'ять. C використовувався там, де продуктивність справді мала значення, і від розробників, як очікували, зрозуміли, наскільки комп'ютери працюють досить добре, щоб знати, що насправді буде визначати ці не визначені форми поведінки в їх конкретних системах.

Пізніші мови, такі як Java та C #, віддають перевагу виключенню невизначеної поведінки над необробленою продуктивністю.


-5

У певному сенсі у неї також є Java. Припустимо, ви дали неправильний компаратор для Arrays.sort. Він може кинути виняток, коли він його виявляє. В іншому випадку він буде сортувати масив певним чином, який не гарантовано буде будь-яким конкретним.

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

C ++ просто пішов далі, щоб створити більше невизначених ситуацій (а точніше, Java вирішила визначити більше операцій) і мати ім’я для цього.


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

2
Що стосується модифікації змінних, то умови перегонів зазвичай не вважаються невизначеною поведінкою. Я не знаю деталей того, як Java обробляє завдання для спільних даних, але знаючи загальну філософію мови, я майже впевнений, що вона повинна бути атомною. Одночасне призначення 53 та 71 aбуло б невизначеною поведінкою, якщо ви могли б отримати 51 або 73 з цього, але якщо ви можете отримати лише 53 або 71, це добре визначено.
Марк

@Mark За допомогою фрагментів даних, більших за розмір нативного слова в системі (наприклад, 32-бітна змінна в 16-бітній системі розміру слів), можливо, є архітектура, яка вимагає зберігання кожної 16-бітної частини окремо. (SIMD - інша потенційна така ситуація.) У цьому випадку навіть просте призначення рівня вихідного коду не обов'язково є атомним, якщо компілятор не дотримується особливої ​​обережності, щоб забезпечити його виконання атомним шляхом.
CVn
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.