Классически компиляторы рассматривали «неопределенное поведение» как просто предлог, чтобы не проверять различные типы ошибок и просто «позволить этому произойти в любом случае». Но современные компиляторы начинают использовать неопределенное поведение, чтобы вести оптимизацию .
Рассмотрим этот код:
int table[5];
bool does_table_contain(int v)
{
for (int i = 0; i <= 5; i++) {
if (table[i] == v) return true;
}
return false;
}
Классические компиляторы не заметят, что ваш предел цикла был написан неправильно и что последняя итерация считывает конец массива. В любом случае он просто попытается прочитать конец массива и вернуть true
, если значение после конца массива совпадало.
С другой стороны, постклассический компилятор может выполнить следующий анализ:
- Первые пять раз в цикле функция может вернуть
true
.
- Когда
i = 5
, код выполняет неопределенное поведение. Поэтому дело i = 5
можно рассматривать как недоступное.
- Случай
i = 6
(цикл продолжается до завершения) также недоступен, потому что для того, чтобы туда добраться, сначала нужно сделать i = 5
, что, как мы уже показали, было недоступно.
- Следовательно, все доступные кодовые пути возвращают
true
.
Компилятор упростит эту функцию до
bool does_table_contain(int v)
{
return true;
}
Другой способ взглянуть на эту оптимизацию состоит в том, что компилятор мысленно развернул цикл:
bool does_table_contain(int v)
{
if (table[0] == v) return true;
if (table[1] == v) return true;
if (table[2] == v) return true;
if (table[3] == v) return true;
if (table[4] == v) return true;
if (table[5] == v) return true;
return false;
}
А потом он понял, что оценка table[5]
не определена, поэтому все, что прошло после этой точки, недостижимо:
bool does_table_contain(int v)
{
if (table[0] == v) return true;
if (table[1] == v) return true;
if (table[2] == v) return true;
if (table[3] == v) return true;
if (table[4] == v) return true;
/* unreachable due to undefined behavior */
}
и затем обратите внимание, что все достижимые пути кода возвращают true
.
Компилятор, который использует неопределенное поведение для управления оптимизацией, увидит, что каждый путь кода через функцию being_a_bad_boy
вызывает неопределенное поведение, и, следовательно, функцию being_a_bad_boy
можно уменьшить до
T& being_a_bad_boy()
{
/* unreachable due to undefined behavior */
}
Этот анализ может затем распространяться на всех абонентов being_a_bad_boy
:
void playing_with_fire(bool match_lit, T& t)
{
kindle(match_lit ? being_a_bad_boy() : t);
}
Поскольку мы знаем, что being_a_bad_boy
недоступен из-за неопределенного поведения, компилятор может заключить, что match_lit
никогда не должен быть true
, что приводит к
void playing_with_fire(bool match_lit, T& t)
{
kindle(t);
}
И теперь все загорается, независимо от того, зажжена ли спичка.
Вы можете не часто видеть этот тип оптимизации с неопределенным поведением в компиляторах текущего поколения, но, как и аппаратное ускорение в веб-браузерах, это только вопрос времени, когда он станет более массовым.