Да, вы правы. Компилятор может делать то, что он хочет, при условии, что наблюдаемое поведение такое же, как и абстрактная машина, которая может выдавать . Но это не драматично само по себе: почему мы заботимся о том, чего нельзя наблюдать? Это точка оптимизации компиляторов.
Пример:
int main() {
int a;
for (int i=INT_MAX; i>=0; i--) {
a = i;
}
printf("%d\n", a);
return 0;
}
Единственное наблюдаемое поведение это то, что он будет печатать 0
за один раз. Таким образом, компилятор может оптимизировать цикл так, чтобы он выглядел так же, как:
int main() {
printf("%d\n", 0);
return 0;
}
Что по сути означает, что вы не можете использовать пустые циклы для добавления задержек, потому что их можно было бы оптимизировать без задержки вообще.
ИМХО самый драматический побочный эффект, если компилятору разрешено предполагать, что в программе не может быть неопределенного поведения.
Второй пример:
int main() {
struct {
int a[16];
int b[16];
} s;
for (i=0; i<16; i++) {
s.a[i] = i;
s.b[i] = 2 * i;
}
for (i=0; i<32; i++) {
printf(" %d", s.a[i]); // UB array access past upper bound
}
printf("\n");
return 0;
}
Наивный компилятор должен отображать все числа от 0 до 31, потому что мы знаем, что массивы s.a
и s.b
должны быть смежными, а арифметика указателей должна давать &(s.b[0]) == &(s.a[16])
. Но оптимизирующий компилятор может заметить, что значения s.b
никогда не используются в наблюдаемом поведении, если не задействован ни один UB, и он может оптимизировать доступ к массиву s.b
и даже оптимизировать член b
. Здесь следует ожидать аварийных или случайных значений ... Хуже того, действительно умный компилятор может заметить, что в цикле печати есть прошлые связанные обращения. С этого момента поведение программы не определено, и компилятор может, например, остановить цикл после печати 16-го значения. Нет ошибок, но напечатано только 16 значений ...