Дозвольте сказати це чітко: ми не посилаємось на невизначену поведінку в наших програмах . Це ніколи не є хорошою ідеєю, періодом. Є рідкісні винятки з цього правила; наприклад, якщо ви реалізатор бібліотеки, який реалізує офсетоф . Якщо ваша справа підпадає під такий виняток, ви, ймовірно, це вже знаєте. У цьому випадку ми знаємо, що використання неініціалізованих автоматичних змінних є невизначеною поведінкою .
Компілятори стали дуже агресивними з оптимізаціями навколо невизначеної поведінки, і ми можемо знайти багато випадків, коли невизначена поведінка призвела до вад безпеки. Найвідоміший випадок - це, мабуть, видалення нульового вказівника ядра Linux, яке я згадую у своїй відповіді на помилку компіляції C ++? де оптимізація компілятора навколо невизначеної поведінки перетворила кінцевий цикл у нескінченний.
Ми можемо прочитати небезпечну оптимізацію та втрату причинності CERT ( відео ), в якому, серед іншого, сказано:
Все частіше автори-компілятори використовують переваги невизначеної поведінки на мовах програмування C та C ++ для покращення оптимізації.
Найчастіше ці оптимізації заважають можливості розробників виконувати аналіз причинно-наслідкового аналізу над своїм вихідним кодом, тобто аналізуючи залежність подальших результатів від попередніх результатів.
Отже, ці оптимізації усувають причинність у програмному забезпеченні та збільшують ймовірність помилок, дефектів та вразливих програм.
Зокрема, щодо невизначених значень стандартний звіт про дефекти С 451: Нестабільність неініціалізованих автоматичних змінних викликає цікаве прочитання. Це ще не було вирішено, але вводиться поняття коливальних значень, що означає, що невизначеність значення може поширюватися через програму і може мати різні невизначені значення в різних точках програми.
Я не знаю жодного прикладу, коли це відбувається, але на даний момент ми не можемо цього виключити.
Реальні приклади, а не результат, який ви очікуєте
Ви навряд чи отримаєте випадкові значення. Компілятор може повністю оптимізувати віддалений цикл. Наприклад, у цьому спрощеному випадку:
void updateEffect(int arr[20]){
for(int i=0;i<20;i++){
int r ;
arr[i] = r ;
}
}
кланг оптимізує його подалі ( дивіться його наживо ):
updateEffect(int*): # @updateEffect(int*)
retq
або можливо отримати всі нулі, як у цьому модифікованому випадку:
void updateEffect(int arr[20]){
for(int i=0;i<20;i++){
int r ;
arr[i] = r%255 ;
}
}
побачити його наживо :
updateEffect(int*): # @updateEffect(int*)
xorps %xmm0, %xmm0
movups %xmm0, 64(%rdi)
movups %xmm0, 48(%rdi)
movups %xmm0, 32(%rdi)
movups %xmm0, 16(%rdi)
movups %xmm0, (%rdi)
retq
Обидва ці випадки є цілком прийнятними формами невизначеної поведінки.
Зауважте, якби ми знаходимось на Itanium, ми могли б отримати значення пастки :
[...] якщо в реєстрі відбувається спеціальне несуттєве значення, читаючи пастки регістра, за винятком кількох інструкцій [...]
Інші важливі зауваження
Цікаво відзначити різницю між gcc та clang, відзначеною в проекті UB Canaries, щодо того, наскільки вони готові скористатися невизначеною поведінкою щодо неініціалізованої пам'яті. Стаття зазначає ( моє наголос ):
Звичайно, ми повинні бути повністю зрозуміли, що будь-яке таке очікування не має нічого спільного з мовним стандартом, і все, що стосується того, що відбувається з певним компілятором, або тому, що постачальники цього компілятора не бажають використовувати цей UB або просто тому що вони ще не взялися до експлуатації . Коли жодної реальної гарантії від постачальника компіляторів не існує, ми хочемо сказати, що поки що невикористані UB - це бомби часу : вони чекають виходу з наступного місяця чи наступного року, коли компілятор стане трохи більш агресивним.
Як зазначає Матьє М., що кожен програміст C повинен знати про не визначену поведінку №2 / 3 , також має відношення до цього питання. У ньому сказано серед іншого ( наголос мій ):
Важливо і страшно зрозуміти, що практично будь-яка
оптимізація, заснована на не визначеній поведінці, може почати спрацьовувати на баггі-код у будь-який час у майбутньому . Розгортання циклу, розгортання циклу, розширення пам’яті та інші оптимізації продовжуватимуть покращуватися, і значна частина їхніх причин існує в тому, щоб відкрити вторинні оптимізації, як описано вище.
Для мене це глибоко невдоволено, частково через те, що компілятор неминуче закінчується звинуваченням, але також тому, що це означає, що величезні тіла коду С - це наземні міни, які лише чекають вибуху.
Для повноти я, мабуть, повинен зазначити, що реалізація може вибрати чітко визначену поведінку, наприклад, gcc дозволяє набирати покарання через союзи, тоді як у C ++ це здається невизначеною поведінкою . У такому випадку реалізація повинна документувати це, і зазвичай це не буде портативним.