为什么常量expression式有未定义行为的排除?

我正在研究核心常量expression式*中允许的内容, C ++标准草案第5.19节的常量expression式中包含了以下内容:

一个条件expression式是一个核心常量expression式,除非它涉及以下之一作为一个潜在的评估子expression式(3.2),但是没有被评估的逻辑AND(5.14),逻辑OR(5.15)和条件(5.16)操作的子expression式不考虑[注意:重载操作符调用一个函数。

并列出了下面的子弹中的排除项(包括我的重点 ):

– 注意:例如,包括有符号整数溢出(第5章),某些指针算术(5.7),除以零(5.6)或某些移位操作(5.8) – 结束注释]。

? 为什么常量expression式需要这个子句来覆盖未定义的行为 ? 常量expression式有什么特别之处,需要不确定的行为才能排除在特殊情况之外呢?

这个条款有没有给我们带来任何好处或工具?

作为参考,这看起来像广义常量expression式的最后一个版本。

措辞实际上是缺陷报告#1313的主题:

对于常量expression式的要求目前不应该但是应该排除那些具有未定义行为的expression式,例如当指针不指向相同数组的元素时的指针算术。

这个决议是我们目前的措词,所以这显然是有意的,那么这是什么工具给我们呢?

让我们看看当我们尝试创build一个包含未定义行为的expression式的constexprvariables时会发生什么,我们将使用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中检测到未定义的行为 ,或者至lessclang认为是未定义的 。 注意, gcc行为是相同的,除了在右移和左移的情况下未定义的行为, gcc通常会在这些情况下产生一个警告,但是仍然将该expression看作是常量。

我们可以通过SFINAE使用这个function来检测一个加法expression式是否会导致溢出,下面这个人为的例子受到了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 

标准要求这种行为并不明显,但霍华德·欣南特(Howard Hinnant)的评论显然确实是这样的:

并且也是constexpr,这意味着UB在编译时被捕获

更新

不知何故,我错过了第695期编译时计算错误的constexpr函数 ,这个错误反映了第5节第4段的用语( 重点是我的前进 ):

如果在expression式评估过程中,结果不是math定义的,或者不在其types的可表示值范围内,则行为是未定义的, 除非这样的expression式出现在需要整型常量expression式的地方(5.19 [expr.const] ),在这种情况下,该计划是不正确的

并继续说:

打算作为“编译时评估”的一个可接受的标准迂回,这个概念并没有被标准直接定义。 这个公式不足以涵盖constexpr函数。

后面的笔记说:

[…]在编译时想要诊断错误与不诊断在运行时实际上不会发生的错误之间存在紧张关系[…] CWG的共识是像1/0这样的expression应该简单地被认为是非恒定的; 任何诊断都会在需要一个常量expression式的上下文中使用expression式。

如果我正确阅读确认其意图是能够在需要一个常量expression式的上下文中编译时诊断未定义的行为。

我们不能肯定地说这是意图,但强烈暗示这是意图。 clanggcc如何处理未定义的变化确实存在一些疑问。

我提交了一个海湾合作委员会的错误报告:右和左移未定义的行为不是一个错误在一个constexpr 。 虽然看起来这是一致的,但它确实打破了SFINAE,我们可以从我的答案中看到, 是否将constexpr标准库函数作为constexpr对待 , 是否符合编译器扩展? 对于SFINAE用户可观察到的分歧似乎对委员会不利。

当我们谈论未定义的行为时 ,重要的是要记住,标准保留了这些情况下未定义的行为。 它并不禁止实施做出更强有力的保证。 例如,一些实现可以保证有符号整数溢出回绕,而另一些可以保证饱和。

要求编译器处理涉及未定义行为的常量expression式会限制实现可能产生的保证,限制它们产生一些没有副作用的值(标准称为不确定值 )。 这排除了在现实世界中发现的很多扩展保证。

例如,一些实现或伴随标准(即POSIX)可以定义由零整除的行为以生成信号。 这是一个副作用,如果expression式是在编译时计算的,将会丢失。

所以,这些expression式在编译时被拒绝,以避免在执行环境中丢失副作用。

还有一点要从常量expression式中排除未定义的行为:根据定义,常量expression式应该在编译时由编译器进行评估。 允许一个常量expression式来调用未定义的行为将允许编译器本身显示未定义的行为。 而编译器,因为你编译一些邪恶的代码格式化你的硬盘驱动器是不是你想要的。