Позвольте мне сказать это ясно: мы не вызываем неопределенное поведение в наших программах . Это никогда не хорошая идея, точка. Есть редкие исключения из этого правила; например, если вы являетесь разработчиком библиотеки, реализующей offsetof . Если ваше дело подпадает под такое исключение, вы, вероятно, уже знаете это. В этом случае мы знаем, что использование неинициализированных автоматических переменных является неопределенным поведением .
Компиляторы стали очень агрессивными с оптимизацией вокруг неопределенного поведения, и мы можем найти много случаев, когда неопределенное поведение приводило к недостаткам безопасности. Наиболее печально известным случаем, вероятно, является удаление проверки нулевого указателя ядра Linux, о которой я упоминал в своем ответе на ошибку компиляции C ++? где оптимизация компилятора вокруг неопределенного поведения превратила конечный цикл в бесконечный.
Мы можем прочитать « Опасные оптимизации CERT и Потеря причинности» ( видео ), в которых, среди прочего, сказано:
Авторы компиляторов все чаще используют неопределяемое поведение в языках программирования C и C ++ для улучшения оптимизации.
Зачастую эти оптимизации влияют на способность разработчиков выполнять анализ причинно-следственных связей с их исходным кодом, то есть анализировать зависимость последующих результатов от предыдущих результатов.
Следовательно, эти оптимизации устраняют причинно-следственные связи в программном обеспечении и увеличивают вероятность программных сбоев, дефектов и уязвимостей.
В частности, в отношении неопределенных значений стандартный отчет о дефекте C 451: Нестабильность неинициализированных автоматических переменных делает интересным чтение. Это еще не решено, но вводит понятие колеблющихся значений, что означает, что неопределенность значения может распространяться через программу и может иметь разные неопределенные значения в разных точках программы.
Я не знаю ни одного примера, где это происходит, но на данный момент мы не можем это исключить.
Реальные примеры, а не ожидаемый результат
Вы вряд ли получите случайные значения. Компилятор может оптимизировать весь цикл. Например, в этом упрощенном случае:
void updateEffect(int arr[20]){
for(int i=0;i<20;i++){
int r ;
arr[i] = r ;
}
}
clang оптимизирует его ( смотрите вживую ):
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 - это бомбы замедленного действия : они ожидают взрыва в следующем месяце или в следующем году, когда компилятор станет немного более агрессивным.
Как отмечает Матье М., то, что должен знать каждый программист на Си о неопределенном поведении № 2/3, также имеет отношение к этому вопросу. Это говорит среди прочего ( выделение мое ):
Важно осознавать, что любая
оптимизация, основанная на неопределенном поведении, может быть запущена с ошибочным кодом в любое время в будущем . Встраивание, развертывание циклов, продвижение памяти и другие оптимизации будут улучшаться, и значительная часть их причин для существования заключается в предоставлении вторичных оптимизаций, подобных приведенным выше.
На мой взгляд, это вызывает глубокую неудовлетворенность, отчасти потому, что компилятор неизбежно заканчивается обвинением, а также потому, что это означает, что огромные массы кода на C - это мины, просто ожидающие взрыва.
Для полноты картины я, вероятно, должен упомянуть, что реализации могут выбрать, чтобы неопределенное поведение было четко определено, например, gcc позволяет пробивать типы через объединения, тогда как в C ++ это выглядит как неопределенное поведение . В этом случае реализация должна документировать это, и это обычно не будет переносимым.