Почему константные выражения имеют исключение для неопределенного поведения?
Я исследовал, что разрешено в основном константном выражении *, которое описано в разделе 5.19
Постоянные выражения параграф 2 проекта стандарта C++, который гласит:
Условное выражение является основным константным выражением, если оно не включает одно из следующего в качестве потенциально вычисляемого подвыражения (3.2), но подвыражения логических операций И (5.14), логического ИЛИ (5.15) и условных (5.16) операций, которые не оцениваются не учитываются [Примечание: перегруженный оператор вызывает функцию.
и перечисляет исключения в следующих пулях и включает в себя (выделение мое):
- операция, которая будет иметь неопределенное поведение [Примечание: включая, например, целочисленное переполнение со знаком (раздел 5), определенную арифметику указателя (5.7), деление на ноль (5.6) или определенные операции сдвига (5.8) - примечание конца];
А? Почему константным выражениям нужен этот пункт, чтобы охватить неопределенное поведение? Есть ли что-то особенное в константных выражениях, которое требует неопределенного поведения для особой вырезки в исключениях?
Дает ли этот пункт нам какие-либо преимущества или инструменты, которых мы не имели бы без него?
Для справки это выглядит как последняя редакция предложения об обобщенных выражениях констант.
3 ответа
Формулировка на самом деле является предметом сообщения о дефекте № 1313, в котором говорится:
Требования к константным выражениям в настоящее время, но не должны исключать выражения, которые имеют неопределенное поведение, например, арифметику указателей, когда указатели не указывают на элементы одного и того же массива.
Эта резолюция является текущей формулировкой, которую мы имеем сейчас, так что это явно было предназначено, так что же нам дает это?
Давайте посмотрим, что происходит, когда мы пытаемся создать переменную constexpr с выражением, которое содержит неопределенное поведение, мы будем использовать clang
для всех следующих примеров. Этот код ( посмотреть его вживую):
constexpr int x = std::numeric_limits<int>::max() + 1 ;
выдает следующую ошибку:
error: constexpr variable 'x' must be initialized by a constant expression
constexpr int x = std::numeric_limits<int>::max() + 1 ;
^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
note: value 2147483648 is outside the range of representable values of type 'int'
constexpr int x = std::numeric_limits<int>::max() + 1 ;
^
Этот код ( посмотреть его вживую):
constexpr int x = 1 << 33 ; // Assuming 32-bit int
выдает эту ошибку:
error: constexpr variable 'x' must be initialized by a constant expression
constexpr int x = 1 << 33 ; // Assuming 32-bit int
^ ~~~~~~~
note: shift count 33 >= width of type 'int' (32 bits)
constexpr int x = 1 << 33 ; // Assuming 32-bit int
^
и этот код, который имеет неопределенное поведение в функции constexpr:
constexpr const char *str = "Hello World" ;
constexpr char access( int index )
{
return str[index] ;
}
int main()
{
constexpr char ch = access( 20 ) ;
}
выдает эту ошибку:
error: constexpr variable 'ch' must be initialized by a constant expression
constexpr char ch = access( 20 ) ;
^ ~~~~~~~~~~~~
note: cannot refer to element 20 of array of 12 elements in a constant expression
return str[index] ;
^
Хорошо, что это полезно, компилятор может обнаружить неопределенное поведение в constexpr, или, по крайней мере, что clang
Вера не определена. Заметка, gcc
ведет себя так же, за исключением случая неопределенного поведения со сдвигом вправо и влево, gcc
обычно выдает предупреждение в этих случаях, но все равно видит выражение как постоянное.
Мы можем использовать эту функцию через SFINAE, чтобы определить, не вызовет ли переполнение выражение переполнения. Следующий придуманный пример был вдохновлен умным ответом dyp:
#include <iostream>
#include <limits>
template <typename T1, typename T2>
struct addIsDefined
{
template <T1 t1, T2 t2>
static constexpr bool isDefined()
{
return isDefinedHelper<t1,t2>(0) ;
}
template <T1 t1, T2 t2, decltype( t1 + t2 ) result = t1+t2>
static constexpr bool isDefinedHelper(int)
{
return true ;
}
template <T1 t1, T2 t2>
static constexpr bool isDefinedHelper(...)
{
return false ;
}
};
int main()
{
std::cout << std::boolalpha <<
addIsDefined<int,int>::isDefined<10,10>() << std::endl ;
std::cout << std::boolalpha <<
addIsDefined<int,int>::isDefined<std::numeric_limits<int>::max(),1>() << std::endl ;
std::cout << std::boolalpha <<
addIsDefined<unsigned int,unsigned int>::isDefined<std::numeric_limits<unsigned int>::max(),std::numeric_limits<unsigned int>::max()>() << std::endl ;
}
что приводит к ( см. это в прямом эфире):
true
false
true
Не очевидно, что стандарт требует такого поведения, но, по-видимому, этот комментарий Говарда Хиннанта указывает, что это действительно так:
[...] и также является constexpr, то есть UB перехватывается во время компиляции
Обновить
Каким-то образом я пропустил ошибку 695 Ошибка вычисления времени компиляции в функциях constexpr, которая вращается над формулировкой раздела 5
пункт 4, который имел обыкновение говорить (выделение мое продвигается вперед):
Если во время вычисления выражения результат не определен математически или не находится в диапазоне представимых значений для его типа, поведение не определено, если только такое выражение не появляется там, где требуется целочисленное константное выражение (5.19 [expr.const]), в этом случае программа плохо сформирована.
и продолжает говорить:
Предполагается, что в качестве приемлемого стандартного обозначения "оценивается во время компиляции", концепция, которая прямо не определена Стандартом. Не ясно, что эта формулировка адекватно охватывает функции constexpr.
и в более поздней записке говорится:
[...] Существует противоречие между желанием диагностировать ошибки во время компиляции и не диагностировать ошибки, которые на самом деле не будут возникать во время выполнения.[...] Консенсус CWG заключался в том, что выражение типа 1/0 должно быть просто считается непостоянным; любая диагностика будет результатом использования выражения в контексте, требующем постоянного выражения.
что, если я правильно читаю, подтверждает, что мы хотели диагностировать неопределенное поведение во время компиляции в контексте, требующем постоянного выражения.
Мы не можем определенно сказать, что это было намерение, но он настоятельно рекомендует это было. Разница в том, как clang
а также gcc
обработка неопределенных смен действительно оставляет место для сомнений.
Я подал отчет об ошибке gcc: поведение неопределенного сдвига вправо и влево не является ошибкой в constexpr. Хотя кажется, что это соответствует, оно нарушает SFINAE, и из моего ответа мы можем видеть, является ли это соответствующим расширением компилятора для обработки функций стандартной библиотеки не-constexpr как constexpr? что расхождение в реализации, наблюдаемое для пользователей SFINAE, представляется нежелательным для комитета.
Когда мы говорим о неопределенном поведении, важно помнить, что Стандарт оставляет поведение неопределенным для этих случаев. Это не запрещает реализациям делать более сильные гарантии. Например, некоторые реализации могут гарантировать, что знаковое целочисленное переполнение распространяется вокруг, в то время как другие могут гарантировать насыщение.
Требование к компиляторам обрабатывать константные выражения, включающие неопределенное поведение, ограничит гарантии, которые может дать реализация, ограничив их получением некоторого значения без побочных эффектов (то, что в стандарте называется неопределенным значением). Это исключает множество расширенных гарантий, найденных в реальном мире.
Например, некоторые реализации или сопутствующие стандарты (например, POSIX) могут определять поведение интегрального деления на ноль для генерации сигнала. Это побочный эффект, который был бы потерян, если бы выражение вычислялось во время компиляции.
Таким образом, эти выражения отклоняются во время компиляции, чтобы избежать потери побочных эффектов в среде выполнения.
Есть еще один момент исключения неопределенного поведения из константных выражений: константные выражения должны, по определению, оцениваться компилятором во время компиляции. Разрешение константному выражению вызывать неопределенное поведение позволило бы самому компилятору показывать неопределенное поведение. И компилятор, который форматирует ваш жесткий диск, потому что вы компилируете какой-то злой код, вам не нужен.