Это законно для исходного кода, содержащего неопределенное поведение, чтобы сбой компилятора? - программирование
Подтвердить что ты не робот

Это законно для исходного кода, содержащего неопределенное поведение, чтобы сбой компилятора?

Допустим, я собираюсь скомпилировать какой-то плохо написанный исходный код C++, который вызывает неопределенное поведение и, следовательно, (как говорится) "все может случиться".

С точки зрения того, что спецификация языка C++ сочтет приемлемой в "совместимом" компиляторе, включает ли "что-нибудь" в этом сценарии сбой компилятора (или кражу моих паролей, или иное неправильное поведение или ошибку во время компиляции), или область действия неопределенного поведения ограничена определенным образом тем, что может произойти при выполнении результирующего исполняемого файла?

4b9b3361

Ответ 1

Нормативное определение неопределенного поведения выглядит следующим образом:

[defns.undefined]

поведение, для которого настоящий международный стандарт не предъявляет никаких требований

[Примечание: при этом Международном Стандарт опускает любое явное определение поведения или когда программа использует ошибочную конструкцию или ошибочные данные. Допустимый неопределенный поведение варьируется от полного игнорирования ситуации с непредсказуемые результаты, ведущие себя во время перевода или программы выполнение задокументированной характеристикой среды (с выдачей диагностического сообщения или без него), до прекращения перевод или исполнение (с выдачей диагностического сообщение). Многие ошибочные программные конструкции не порождают неопределенных поведение; они должны быть диагностированы. Оценка константы выражение никогда не демонстрирует поведение, явно указанное как неопределенное. - конец примечания]

Хотя сама заметка не является нормативной, в ней описывается ряд поведенческих реализаций, которые, как известно, демонстрируют. Таким образом, сбой компилятора (который внезапно завершается переводом) является допустимым в соответствии с этой запиской. Но на самом деле, как говорится в нормативном тексте, стандарт не устанавливает границ ни для исполнения, ни для перевода. Если реализация украдет ваши пароли, это не является нарушением какого-либо контракта, изложенного в стандарте.

Ответ 2

Большинство типов UB, о которых мы обычно беспокоимся, например, NULL-deref или деление на ноль, это UB времени выполнения. Компиляция функции, которая вызовет выполнение UB во время выполнения, не должна вызывать сбой компилятора. Если, возможно, он не сможет доказать, что функция (и этот путь через функцию) определенно будет выполняться программой.

(2-я мысль: может быть, я не учел необходимость вычисления template/constexpr во время компиляции. Возможно, UB во время этого может вызвать произвольную странность во время перевода, даже если получающаяся функция никогда не вызывается.)

Поведение во время перевода части цитаты ISO C++ в ответе @StoryTeller аналогично языку, используемому в стандарте ISO C. C не включает шаблоны или constexpr обязательный eval во время компиляции.

Но забавный факт: ISO C отмечает в примечании, что если перевод завершен, он должен быть с диагностическим сообщением. Или "ведение себя во время перевода... задокументированным способом". Я не думаю, что "полное игнорирование ситуации" может быть истолковано как прекращение перевода.


Старый ответ, написанный до того, как я узнал о UB времени перевода. Это верно для UB времени выполнения, хотя и, следовательно, потенциально все еще полезно.


Там нет такой вещи, как UB, что происходит во время компиляции. Он может быть виден компилятору по определенному пути выполнения, но в терминах C++ этого не произошло, пока выполнение не достигнет этого пути выполнения через функцию.

Дефекты в программе, которые делают невозможной даже компиляцию, - это не UB, а синтаксические ошибки. Такая программа "не правильно сформирована" в терминологии C++ (если я верен в своем стандарте). Программа может быть правильной, но содержать UB. Разница между неопределенным поведением и плохо сформированным, диагностическое сообщение не требуется

Если только я не неправильно понял что-то, ISO C++ требует, чтобы эта программа компилировалась и выполнялась правильно, потому что выполнение никогда не достигает деления на ноль. (На практике (Godbolt), хорошие компиляторы просто создают рабочие исполняемые файлы. Gcc/clang предупреждает о x / 0, но не об этом, даже при оптимизации. Но в любом случае, мы пытаемся сказать, насколько низкий ISO C++ допускает качество реализации. Поэтому проверка gcc/clang вряд ли является полезным тестом, кроме как для подтверждения того, что я написал программу правильно.)

int cause_UB() {
    int x=0;
    return 1 / x;      // UB if ever reached.
 // Note I'm avoiding  x/0  in case that counts as translation time UB.
 // UB still obvious when optimizing across statements, though.
}

int main(){
    if (0)
        cause_UB();
}

Вариант использования для этого может включать препроцессор C или переменные constexpr и ветвление по этим переменным, что приводит к бессмысленности в некоторых путях, которые никогда не достигаются для этих вариантов выбора констант.

Можно предположить, что пути выполнения, которые вызывают UB, видимый во время компиляции, никогда не будут выбраны, например компилятор для x86 может выдать ud2 (вызвать исключение недопустимой инструкции) в качестве определения для cause_UB(). Или внутри функции, если одна сторона if() приводит к доказуемому UB, ветвь может быть удалена.

Но компилятор все еще должен компилировать все остальное в здравом уме и правильно. Все пути, которые не встречаются (или не могут быть доказаны, чтобы встретить) UB, все равно должны быть скомпилированы в asm, который выполняется так, как если бы абстрактная машина C++ его выполняла.


Можно утверждать, что безусловный UB, видимый во время компиляции, в main является исключением из этого правила. Или иным способом, обеспечивающим время компиляции, что выполнение, начинающееся с main, действительно достигает гарантированного UB.

Я до сих пор утверждаю, что легальное поведение компилятора включает в себя создание гранаты, которая взрывается при запуске. Или, более правдоподобно, определение main, которое состоит из одной незаконной инструкции. Я бы сказал, что если вы никогда не запускаете программу, UB еще не будет. Сам компилятор не может взорваться, IMO.


Функции, содержащие возможные или доказуемые UB внутри ветвей

UB вдоль любого заданного пути выполнения достигает назад во времени, чтобы "загрязнить" весь предыдущий код. Но на практике компиляторы могут воспользоваться этим правилом только тогда, когда они действительно могут доказать, что пути выполнения ведут к UB, видимому во время компиляции. например,

int minefield(int x) {
    if (x == 3) {
        *(char*)nullptr = x/0;
    }

    return x * 5;
}

Компилятор должен создать asm, который работает для всех x, кроме 3, вплоть до точек, где x * 5 вызывает переполнение со знаком UB в INT_MIN и INT_MAX. Если эта функция никогда не вызывается с x==3, программа, конечно, не содержит UB и должна работать как написано.

Мы могли бы также написать if(x == 3) __builtin_unreachable(); в GNU C, чтобы сказать компилятору, что x определенно не 3.

На практике код "минное поле" повсюду в обычных программах. например любое деление на целое число обещает компилятору, что оно ненулевое. Любой указатель deref обещает компилятору, что он не равен NULL.

Ответ 3

Что здесь означает "законный"? Все, что не противоречит стандарту C или C++, является законным, согласно этим стандартам. Если вы выполняете выражение i = i++; и в результате динозавры захватывают мир, это не противоречит стандартам. Однако это противоречит законам физики, поэтому этого не произойдет :-)

Если неопределенное поведение приводит к сбою вашего компилятора, это не нарушает стандарт C или C++. Это, однако, означает, что качество компилятора может (и, вероятно, должно быть) улучшено.

В предыдущих версиях стандарта C были операторы, которые были ошибочными или не зависели от неопределенного поведения:

char* p = 1 / 0;

Назначение константы 0 для символа * разрешено. Разрешение ненулевой константы не является. Поскольку значение 1/0 является неопределенным поведением, не определено поведение, должен ли компилятор принимать или не принимать это утверждение. (В настоящее время 1/0 больше не соответствует определению "целочисленного константного выражения").