Чи можна вважати гілки з невизначеною поведінкою недосяжними та оптимізувати їх як мертвий код?


88

Розглянемо наступне твердження:

*((char*)NULL) = 0; //undefined behavior

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

Чи буде наступна програма чітко визначена на випадок, якщо користувач ніколи не введе номер 3?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}

Або це зовсім невизначена поведінка, незалежно від того, що вводить користувач?

Крім того, чи може компілятор припустити, що невизначена поведінка ніколи не буде виконана під час виконання? Це дозволило б міркувати назад у часі:

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Тут компілятор міг би міркувати, що у випадку, якщо num == 3ми завжди будемо посилатися на невизначену поведінку. Тому цей випадок повинен бути неможливим, а номер не потрібно друкувати. Всю ifзаяву можна оптимізувати. Чи допускається такий вид зворотних міркувань відповідно до стандарту?


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

4
Я думаю, що час, коли виникає невизначена поведінка, є невизначеним.
eerorika

6
Стандарт C ++ прямо говорить, що шлях виконання з невизначеною поведінкою в будь-який момент є абсолютно невизначеним. Я б навіть інтерпретував це як таке, що будь-яка програма з невизначеною поведінкою на шляху є абсолютно невизначеною (що включає розумні результати в інших частинах, але це не гарантується). Компілятори можуть вільно використовувати невизначену поведінку для модифікації вашої програми. blog.llvm.org/2011/05/what-every-c-programmer-should-know.html містить кілька гарних прикладів.
Йенс,

4
@Jens: Це насправді означає лише шлях виконання. В іншому випадку у вас трапляються неприємності const int i = 0; if (i) 5/i;.
MSalters

1
Компілятор загалом не може довести, що PrintToConsoleне викликає, std::exitтому він повинен зробити дзвінок.
MSalters

Відповіді:


65

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

Ні того, ні іншого. Перша умова занадто сильна, а друга занадто слабка.

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

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

Це можна інтерпретувати:

Якщо виконання програми дає невизначену поведінку, то вся програма має невизначену поведінку.

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

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

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

Ви можете думати про це як про "UB має машину часу".

Зокрема, щоб відповісти на ваші приклади:

  • Поведінка не визначена, лише якщо прочитано 3.
  • Компілятори можуть і навіть усувають код як мертвий, якщо базовий блок містить операцію, яка, безумовно, не визначена. Вони дозволені (і я здогадуюсь) у випадках, які не є основним блоком, але коли всі гілки ведуть до UB. Цей приклад не є кандидатом, якщо PrintToConsole(3)не відомо, що він обов’язково повернеться. Це може призвести до винятку чи чогось іншого.

Подібним прикладом вашого другого є варіант gcc -fdelete-null-pointer-checks, який може приймати такий код (я не перевіряв цей конкретний приклад, вважаю його ілюстративним для загальної ідеї):

void foo(int *p) {
    if (p) *p = 3;
    std::cout << *p << '\n';
}

і змініть його на:

*p = 3;
std::cout << "3\n";

Чому? Оскільки якщо pзначення null, тоді в коді так чи інакше є UB, тому компілятор може припустити, що він не є null і відповідно оптимізувати. Лінукс ядро спіткнувся це ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) , по суті , так як він працює в режимі , коли разименованія покажчика NULL НЕ передбачається як UB, очікується, що це призведе до визначеного апаратного винятку, який ядро ​​може обробити. Коли ввімкнено оптимізацію, gcc вимагає використання -fno-delete-null-pointer-checks, щоб надати цю нестандартну гарантію.

PS Практична відповідь на питання "коли вражає невизначена поведінка?" "10 хвилин до того, як ви планували виїхати на день".


4
Власне, в минулому через це було досить багато проблем із безпекою. Зокрема, будь-яка перевірка фактичного переповнення загрожує за рахунок цього оптимізацією. Наприклад , void can_add(int x) { if (x + 100 < x) complain(); }можна оптимізувати геть повністю, тому що якщо x+100 Байдуже » переповнення нічого не відбувається, а якщо x+100 ж переповнення, що це UB в відповідності зі стандартом, так що нічого не може трапитися.
fgp

3
@fgp: вірно, це оптимізація, на яку люди гірко скаржаться, якщо натрапляють на неї, бо починає здаватися, що компілятор навмисно порушує ваш код, щоб покарати вас. "Чому б я написав це так, якби хотів, щоб ти його видалив!" ;-) Але я думаю, що іноді оптимізатору корисно маніпулювати більшими арифметичними виразами, припустити, що немає переповнення та уникнути чогось дорогого, що було б потрібно лише у цих випадках.
Steve Jessop

2
Чи правильно було б сказати, що програма не визначена, якщо користувач ніколи не вводить 3, але якщо він вводить 3 під час виконання, то все виконання стає невизначеним? Як тільки буде на 100% впевненим, що програма буде викликати невизначену поведінку (і не раніше цього), поведінка стає дозволеною що завгодно. Чи мої ці твердження на 100% правильні?
usr

3
@usr: Я вважаю, що це правильно, так. З вашим конкретним прикладом (і висловлюючи деякі припущення щодо неминучості оброблюваних даних), я думаю, реалізація могла б, в принципі, дивитися вперед у буферизованому STDIN, 3якщо він цього хоче, і збиратись додому на день, як тільки побачила вхідні.
Steve Jessop

3
Додатковий +1 (якщо міг) для вашого PS
Фред Ларсон

10

Стандарт зазначає на рівні 1,9 / 4

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

Цікавим моментом є, мабуть, те, що означає "містити". Трохи пізніше в 1.9 / 5 там сказано:

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

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

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


1
Якщо його сприймати буквально, це смертний вирок для всіх існуючих реальних програм.
usr

6
Я не думаю, що питання полягало в тому, чи може UB з’явитися до того, як насправді буде отримано код. Питання, наскільки я зрозумів, полягало в тому, чи може з’явитися UB, якщо код навіть не буде досягнутий. І звичайно, відповідь на це - "ні".
sepp2k

Ну, стандарт не настільки чіткий щодо цього в 1.9 / 4, але 1.9 / 5, можливо, можна інтерпретувати як те, що ви сказали.
Danvil

1
Примітки не є нормативними. 1.9 / 5 перевершує ноту в 1.9 / 4
MSalters

5

Повчальним прикладом є

int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}

Як поточний GCC, так і поточний Clang оптимізують це (на x86) до

xorl %eax,%eax
ret

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


1
Цікавий приклад. Досить неприємно, що ввімкнення оптимізації приховує попередження. Це навіть не задокументовано - у документах GCC лише сказано, що ввімкнення оптимізації дає більше попереджень.
sleske

@sleske Це неприємно, я погоджуюсь, але неініціалізовані попередження про цінності, як відомо, важко "оправити" - їх ідеальне виконання рівнозначно Проблемі зупинки, і програмісти стають дивно ірраціональними щодо додавання "непотрібних" змінних ініціалізацій для гасіння помилкових спрацьовувань, тому автори компіляторів опиняються над бочкою. Раніше я зламував GCC, і я пам’ятаю, що всі боялися возитися з пропуском попередження про неініціалізовану вартість.
zwol

@zwol: Цікаво, наскільки велика частина "оптимізації", яка виникає в результаті такого усунення мертвого коду, насправді робить корисний код меншим, і скільки в кінцевому підсумку змушує програмістів робити код більшим (наприклад, додаючи код для ініціалізації, aнавіть якщо за будь-яких обставин неініціалізований aпереходить до функції, яка ніколи з цим нічого не зробить)?
supercat

@supercat Я не був глибоко залучений до роботи з компіляторами ~ 10 років, і майже неможливо міркувати про оптимізацію на прикладах іграшок. Цей тип оптимізації, як правило, пов’язаний із загальним зменшенням розміру коду на 2-5% у реальних додатках, якщо я правильно пам’ятаю.
zwol

1
@supercat 2-5% величезний, оскільки ці речі йдуть. Я бачив, як люди потіють на 0,1%.
zwol

4

У поточному робочому проекті C ++ в 1.9.4 сказано, що

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

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

Є дві справді хороші статті про невизначену поведінку та те, що зазвичай роблять компілятори:


1
Це безглуздя. Функція, int f(int x) { if (x > 0) return 100/x; else return 100; }звичайно, ніколи не викликає невизначену поведінку, хоча 100/0, звичайно, невизначена.
fgp

1
@fgp Однак стандарт (особливо 1.9 / 5) говорить, що якщо можна досягти невизначеної поведінки , то не має значення, коли вона буде досягнута. Наприклад,printf("Hello, World"); *((char*)NULL) = 0 не гарантовано що-небудь надрукувати. Це допомагає оптимізувати, оскільки компілятор може вільно переупорядковувати операції (звичайно, з урахуванням обмежень залежностей), які, як він знає, відбудуться з часом, не беручи до уваги невизначену поведінку.
fgp

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

1
Точно - отже, важливим є те, чи справді може бути спрацьований UB чи ні, а не те, чи може він теоретично бути запущений. Або ви готові сперечатися, що int x,y; std::cin >> x >> y; std::cout << (x+y);дозволено говорити, що "1 + 1 = 17", просто тому, що є деякі входи, де x+yпереповнюється (що є UB, оскільки intє підписаним типом).
fgp

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

3

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

Ілюстрація:

*ptr = 0;

Це невизначена поведінка? Припустимо, ми на 100% впевнені ptr == nullptrхоча б раз під час виконання програми. Відповідь повинна бути ствердною.

Як що до цього?

 if (ptr) *ptr = 0;

Це невизначено? (Пам'ятаєте ptr == nullptrхоча б раз?) Я сподіваюся, що ні, інакше ви взагалі не зможете написати жодної корисної програми.

Жоден срандардесе не постраждав при прийнятті цієї відповіді.


3

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

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Якщо компілятор не знає визначення PrintToConsole, він не може видалити if (num == 3)умовний. Припустимо, що у вас є LongAndCamelCaseStdio.hсистемний заголовок із наступною декларацією PrintToConsole.

void PrintToConsole(int);

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

int printf(const char *, ...);
void exit(int);

void PrintToConsole(int num) {
    printf("%d\n", num);
    exit(0);
}

Компілятор насправді повинен припустити, що будь-яка довільна функція, яку компілятор не знає, що він робить, може вийти або створити виняток (у випадку C ++). Ви можете помітити, що *((char*)NULL) = 0;це не буде виконано, оскільки виконання не триватиме після PrintToConsoleдзвінка.

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

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

int putchar(int);

const char *warning;

void lol_null_check(const char *pointer) {
    if (!pointer) {
        warning = "pointer is null";
    }
    putchar(*pointer);
}

У цьому випадку легко помітити, що lol_null_checkпотрібен покажчик, що не має значення NULL. Присвоєння глобальній енергонезалежній warningзмінній - це не те, що може вийти з програми або викликати будь-який виняток. pointerТакож незалежна, тому він не може чарівним чином змінити його значення в середині функції (якщо він робить, це невизначене поведінку). Виклик lol_null_check(NULL)спричинить невизначену поведінку, яка може спричинити непризначення змінної (оскільки на даний момент відомий той факт, що програма виконує невизначену поведінку).

Однак невизначена поведінка означає, що програма може робити що завгодно. Отже, ніщо не заважає невизначеній поведінці повернутися назад у часі і збити програму перед першим рядкомint main() . Це невизначена поведінка, вона не повинна мати сенсу. Можливо, він також вийде з ладу після введення 3, але невизначена поведінка повернеться назад у часі і вийде з ладу ще до того, як ви введете 3. І хто знає, можливо, невизначена поведінка перезапише вашу оперативну пам’ять і призведе до збою системи через 2 тижні, поки ваша невизначена програма не працює.


Усі діючі бали. PrintToConsoleце моя спроба вставити зовнішній побічний ефект програми, який видно навіть після збоїв і є сильно послідовно. Я хотів створити ситуацію, коли ми можемо точно сказати, чи оптимізовано це твердження. Але ти маєш рацію в тому, що воно може ніколи не повернутися .; Ваш приклад написання до глобальної мережі може бути предметом інших оптимізацій, які не пов’язані з UB. Наприклад, невикористаний глобальний можна видалити. У вас є ідея створити зовнішній побічний ефект таким чином, щоб гарантовано повернути контроль?
usr

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

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

1

Якщо програма досягне твердження, яке викликає невизначену поведінку, жодні вимоги до програми / поведінки програми не пред'являються; не має значення, чи відбуватимуться вони "до" чи "після" виклику невизначеної поведінки.

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


1
Коли з цікавості __builtin_unreachable()почали з’являтися ефекти, які рухались і назад, і вперед у часі? Враховуючи щось подібне, extern volatile uint32_t RESET_TRIGGER; void RESET(void) { RESET_TRIGGER = 0xAA55; __memorybarrier(); __builtin_unreachable(); }я міг би вважати builtin_unreachable(), що це добре, щоб повідомити компілятору, що він може опустити returnінструкцію, але це було б зовсім інакше, ніж сказати, що попередній код може бути опущений.
суперкіт

@supercat, оскільки RESET_TRIGGER нестабільний, запис у це місце може мати довільні побічні ефекти. Для компілятора це як непрозорий виклик методу. Тому неможливо довести (і це не так) __builtin_unreachableдосягнення. Ця програма визначена.
usr

@usr: Я вважаю, що низькорівневі компілятори повинні розглядати мінливий доступ як непрозорі виклики методів, але ні clang, ні gcc цього не роблять. Крім усього іншого, непрозорий виклик методу може призвести до того, що всі байти будь-якого об'єкта, адреса якого потрапив у зовнішній світ, і до якого не було доступу, а також не буде доступний живий restrictпокажчик, будуть записані за допомогою unsigned char*.
supercat

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

@supercat Я думаю, ти маєш рацію. Здається, мінливі операції "не впливають на абстрактну машину" і тому не можуть припинити програму або викликати побічні ефекти.
usr

1

Багато стандартів для багатьох видів речей витрачають багато зусиль на опис речей, реалізація яких НЕ ПОВИННА або НЕ ПОВИННА робити, використовуючи номенклатуру, подібну до тієї, що визначена в IETF RFC 2119 (хоча не обов'язково посилаючись на визначення в цьому документі). У багатьох випадках описи речей, які повинні виконувати реалізації, за винятком випадків, коли вони були б марними або непрактичними , важливіші за вимоги, до яких повинні відповідати всі відповідні реалізації.

На жаль, стандарти C і C ++ уникають описів речей, які, хоча і не є 100% необхідними, все ж слід очікувати від якісних реалізацій, які не документують протилежної поведінки. Припущення про те, що реалізації повинні щось робити, може сприйматися як таке, що означає, що ті, які не є нижчими, і в тих випадках, коли загалом було б очевидно, яка поведінка була б корисною чи практичною, а не непрактичною та марною, для даної реалізації, було мало відчутна необхідність того, щоб Стандарт втручався у такі судження.

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


Хорошим моментом є те, що уникання UB може призвести до витрат на код користувача.
usr

@usr: Це момент, який модерністи повністю пропускають. Чи варто додати приклад? Наприклад, якщо код повинен оцінювати, x*y < zколи x*yвін не переповнюється, а у випадку переповнення виводить або 0, або 1 довільним чином, але без побічних ефектів, на більшості платформ немає причин, чому задоволення другої та третьої вимог має бути дорожчим, ніж задоволення першого, але будь-який спосіб написання виразу, щоб гарантувати стандартну поведінку у всіх випадках, в деяких випадках додасть значних витрат. Написання виразу (int64_t)x*y < zмогло б збільшити вартість обчислень у чотири рази ...
supercat

... на деяких платформах і писати це так, як (int)((unsigned)x*y) < zце заважало б компілятору застосовувати те, що в іншому випадку могло б бути корисним алгебраїчним заміщенням (наприклад, якщо він знає це xі zє рівним і позитивним, це може спростити вихідний вираз до y<0, але версія з використанням unsigned змусив би компілятор виконати множення). Якщо компілятор може гарантувати, хоча Стандарт не вимагає цього, він буде підтримувати вимогу "вихід 0 або 1 без побічних ефектів", користувацький код може дати компілятору можливості оптимізації, яких він не міг отримати в іншому випадку.
supercat

Так, здається, якась м’якша форма невизначеної поведінки тут була б корисною. Програміст може ввімкнути режим, який викликає x*yнормальне значення у випадку переповнення, але будь-яке значення взагалі. Настроюваний UB в C / C ++ мені здається важливим.
usr

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