Почему константные выражения имеют исключение для поведения undefined?

Я изучал, что разрешено в ядро ​​константного выражения *, которое рассматривается в разделе 5.19 Постоянные выражения, параграф 2 из черновик стандарта С++, в котором говорится:

Условное выражение является выражением основной константы, если оно не включает одно из следующих значений в качестве потенциально оцениваемого подвыражения (3.2), но подвыражения логических операций AND (5.14), логического ИЛИ (5.15) и условного (5.16), которые не оцениваются, не рассматриваются [Примечание: перегруженный оператор вызывает функцию.-end note]:

и перечисляет исключения в последующих марках и включает (выделение мое):

- операция, которая имела бы поведение undefined [Примечание: включая, например, целочисленное переполнение цепочки (раздел 5), определенную арифметику указателя (5.7), деление на ноль (5.6), или некоторые операции сдвига (5.8) -end note];

А? Почему константные выражения нуждаются в этом разделе для описания undefined поведения? Есть ли что-то особенное в отношении постоянных выражений, которые требуют, чтобы поведение undefined было выделено в исключениях?

Имеет ли данное предложение какие-либо преимущества или инструменты, которые у нас не были бы без него?

Для справки это выглядит как последняя версия предложения для Обобщенные константные выражения.

Ответ 1

Формулировка на самом деле является темой отчет о дефекте # 1313, в котором говорится:

Требования к постоянным выражениям в настоящее время не нужны, но должны исключать выражения, которые имеют поведение undefined, такие как арифметика указателя, когда указатели не указывают на элементы одного и того же массива.

Разрешение, являющееся текущей формулировкой, которую мы имеем сейчас, поэтому это явно предназначалось, поэтому какие инструменты нам дают?

Посмотрим, что произойдет, когда мы попытаемся создать переменную constexpr с выражением, содержащим поведение undefined, мы будем использовать 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
                  ^

и этот код, который имеет поведение undefined в функции 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] ;
           ^

Хорошо, что компилятор может обнаружить поведение undefined в constexpr, или, по крайней мере, то, что clang считает undefined. Обратите внимание: gcc ведет себя так же, как в случае поведения undefined с правым и левым сдвигом, 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, который использовался сказать (акцент мой вперед):

Если во время оценки выражения результат не определяется математически или нет в диапазоне представляемых значений для его типа, поведение undefined, , если не появляется такое выражение, где интегральное постоянное выражение требуется (5.19 [expr.const]), и в этом случае программа плохо сформирована.

и далее:

предназначенный как приемлемое стандартное правило для "оцениваемых во время компиляции", понятие, которое не определено непосредственно Стандартом. Непонятно, что эта формулировка адекватно охватывает функции constexpr.

и более поздняя заметка гласит:

[...] Существует напряженность между желанием диагностировать ошибки во время компиляции и не диагностировать ошибки, которые на самом деле не будут возникать во время выполнения. [...] Консенсус CWG заключался в том, что выражение типа 1/0 следует просто считать непостоянным; любая диагностика может возникнуть в результате использования выражения в контексте, требующем постоянного выражения.

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

Мы не можем однозначно сказать, что это было намерение, но это действительно означает, что это так. Разница в том, как clang и gcc относятся к сдвигам undefined, оставляет сомнение.

Я подал отчет об ошибке gcc: поведение вправо и влево undefined не является ошибкой в ​​constexpr. Хотя похоже, что это соответствует, оно нарушает SFINAE, и мы можем видеть из моего ответа на Является ли это совместимым расширением компилятора для обработки стандартных библиотечных функций, не являющихся constexpr, как constexpr? что расхождение в реализации, наблюдаемое для пользователей SFINAE, представляется нежелательным для комитета.

Ответ 2

Когда мы говорим о поведении undefined, важно помнить, что стандарт оставляет поведение undefined для этих случаев. Он не запрещает реализации более сильных гарантий. Например, некоторые реализации могут гарантировать, что подписанное целочисленное переполнение обернется вокруг, в то время как другие могут гарантировать насыщенность.

Требование, чтобы компиляторы обрабатывали константные выражения с использованием поведения undefined, ограничивали бы гарантии, которые могла бы выполнить реализация, ограничивая их получением некоторого значения без побочных эффектов (что Стандарт вызывает неопределенное значение). Это исключает множество расширенных гарантий, найденных в реальном мире.

Например, некоторый стандарт реализации или сопутствующего устройства (то есть POSIX) может определять поведение интегрального деления на ноль для генерации сигнала. Это побочный эффект, который был бы потерян, если бы выражение было вычислено во время компиляции.

Таким образом, эти выражения отклоняются во время компиляции, чтобы избежать потери побочных эффектов в среде выполнения.

Ответ 3

Есть еще один момент для исключения поведения undefined из константных выражений: константные выражения должны, по определению, оцениваться компилятором во время компиляции. Разрешить постоянное выражение для вызова поведения undefined позволило бы самому компилятору показать поведение undefined. И компилятор, который форматирует ваш жесткий диск, потому что вы компилируете какой-то злой код, не является тем, что вы хотите иметь.