为什么常量表达式会排除未定义的行为?

Sha*_*our 46 c++ sfinae undefined-behavior constexpr c++11

我正在研究核心常量表达式*中允许的内容,这在C++标准草案的5.19 常量表达式2段中有所描述:

条件表达式是核心常量表达式,除非它涉及以下之一作为潜在评估的子表达式(3.2),但是未评估的逻辑AND(5.14),逻辑OR(5.15)和条件(5.16)操作的子表达式不考虑[注意:重载的运算符调用函数.-end note]:

并列出随后的子弹中的排除项并包括(强调我的):

- 具有未定义行为的操作 [注意:包括,例如,有符号整数溢出(第5条),某些指针算术(5.7),除零(5.6)或某些移位操作(5.8) - 结束注释];

?为什么常量表达式需要此子句来涵盖未定义的行为常量表达式是否有一些特殊的东西需要未定义的行为才能在排除中进行特殊划分?

拥有这个条款是否给了我们没有它的任何优势或工具?

作为参考,这看起来像广义常量表达式提案的最新修订版.

Sha*_*our 39

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

常量表达式的要求当前不应该但是应该排除具有未定义行为的表达式,例如当指针不指向同一数组的元素时的指针算法.

该决议是我们现在的措辞,所以这显然是有意的,那么这给了我们什么样的工具呢?

让我们看看当我们尝试使用包含未定义行为的表达式创建constexpr变量时会发生什么,我们将使用以下所有示例.这段代码(现场直播):clang

constexpr int x = std::numeric_limits<int>::max() + 1 ;
Run Code Online (Sandbox Code Playgroud)

产生以下错误:

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 ;
                                       ^
Run Code Online (Sandbox Code Playgroud)

这段代码(现场直播):

constexpr int x = 1 << 33 ;  // Assuming 32-bit int
Run Code Online (Sandbox Code Playgroud)

产生此错误:

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
                  ^
Run Code Online (Sandbox Code Playgroud)

这个代码在constexpr函数中有未定义的行为:

constexpr const char *str = "Hello World" ;      

constexpr char access( int index )
{
    return str[index] ;
}

int main()
{
    constexpr char ch = access( 20 ) ;
}
Run Code Online (Sandbox Code Playgroud)

产生此错误:

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] ;
           ^
Run Code Online (Sandbox Code Playgroud)

嗯,这是非常有用的编译器可以检测到未定义的行为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 ;
}
Run Code Online (Sandbox Code Playgroud)

结果(见它直播):

true
false
true
Run Code Online (Sandbox Code Playgroud)

标准要求这种行为并不明显,但霍华德·亨南特的评论显然确实如此:

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

更新

不知怎的,我错过了第695期的constexpr函数中的编译时计算错误,这些错误围绕着54节的措辞(过去强调我的未来):

如果在评估表达式期间,结果未在数学上定义或未在其类型的可表示值范围内,则行为未定义,除非出现需要整数常量表达式的表达式(5.19 [expr.const]) ),在这种情况下,该程序是不正确的.

接着说:

旨在作为"在编译时评估"的可接受的标准附录,这个概念不是由标准直接定义的.目前尚不清楚这种配方是否足以涵盖constexpr功能.

后来的说明说:

[...]在编译时想要诊断错误与不诊断在运行时实际不会发生的错误之间存在紧张关系.[...] CWG的共识是,像1/0这样的表达应该只是被认为是非常数; 在需要常量表达的上下文中使用表达式会导致任何诊断.

如果我正确阅读,则确认其意图是能够在需要常量表达的上下文中在编译时诊断未定义的行为.

我们不能肯定地说这是意图,但强烈暗示它是.在怎么区别clanggcc对待不确定的变化并留下怀疑的余地.

我提交了一个gcc错误报告:右移和左移未定义的行为不是constexpr中的错误.虽然看起来这符合要求,但确实打破了SFINAE,我们可以从我的回答中看到它是否是一个符合编译器的扩展,将非constexpr标准库函数视为constexpr?委员会认为SFINAE用户可观察到的实施差异似乎是不可取的.

  • @ZacHowland请看[我可以回答我自己的问题,即使我在问之前就知道了答案吗?](http://meta.stackexchange.com/questions/17463/can-i-answer-my-own-questions-even - 如果-I-知道最答案前方的要求) (21认同)
  • 不幸的是,你所拥有的似乎是一个幸运的协同作用,而不是从一开始就从"constexpr"评估中排除未定义行为的理由,因此不能回答你提出的问题.(DR中没有提供任何理由) (2认同)

Ben*_*igt 21

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

要求编译器处理涉及未定义行为的常量表达式将限制实现可以做出的保证,限制它们产生一些没有副作用的值(标准称为不确定值).这排除了现实世界中发现的许多延伸保证.

例如,一些实现或伴随标准(即POSIX)可以将积分除以零的行为定义为生成信号.如果表达式是在编译时计算的,那么这会产生副作用.

因此,这些表达式在编译时被拒绝,以避免在执行环境中丢失副作用.

  • 可悲的是,我不知道这是否是委员会所考虑的理由.但这就是我同意这条规则的原因. (5认同)
  • +1谢谢你,我希望得到一个实际进入理由的答案.我花了很多时间试图找到昨晚的理由,但我认为没有任何公开的内容可以涵盖它. (2认同)

cma*_*ter 6

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

  • 编译器不会"调用"常量表达式,它们会被"处理".这种处理没有理由来格式化您的硬盘.涉及未定义行为的常量表达式通常会处理为不确定的值.但这会阻止实现对标准未定义的行为做出强有力的保证. (6认同)
  • 我认为在所有上下文*中,在编译时*都不需要*常量表达式*.对于数组边界等,显然这是"必需的"隐含的,但我不确定是否有一般要求. (3认同)