为什么常数表达式对不确定的行为有排除

Why do constant expressions have an exclusion for undefined behavior?

本文关键字:排除 不确定 常数 表达式 为什么      更新时间:2023-10-16

i正在研究核心常数表达式*中允许的内容,该内容在5.19 contert ancters extressions 段落C 标准草案说:

有条件表达是核心常数表达式,除非它涉及以下一个潜在评估的子表达(3.2),但是逻辑和(5.14),逻辑或(5.15)和条件(5.16)操作(5.16)操作(5.14)的子表达未评估未考虑[注意:一个超载的操作员调用功能。

并列出了随后的子弹中的排除,包括(重点是我的):

- 一个将具有未定义行为的操作 [注意:例如,包括签名的整数溢出(第5条),某些指针算术(5.7),零(5.6)或某些划分班次操作(5.8) - 末尾注];

huh ?为什么常数表达式需要此条款涵盖未定义的行为?是否有关于常数表达式的特殊内容,需要不确定的行为在排除中有一个特殊的雕刻?

拥有此子句是否给我们任何我们将没有的优点或工具吗?

供参考,这看起来像是通用常数表达式提案的最后修订。

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

当前对恒定表达式的要求不是,但应排除具有不确定行为的表达式,例如指针不指向同一数组的元素时。

解决方案是我们现在拥有的当前措辞,所以这显然是意图的,所以这有什么工具给我们?

让我们看看当我们尝试创建 constexpr variable 的表达式时,会发生什么,该表达式包含 undection行为,我们将在以下所有示例中使用clang。此代码(请参阅Live ):

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 ;
                                       ^

此代码(请参阅Live ):

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认为是 Undefined 。请注意,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 ;
}

导致(请参阅Live ):

true
false
true

很明显,标准需要这种行为,但显然霍华德·辛南特(Howard Hinnant)的这一评论表明确实是:

[...],也是constexpr,这意味着UB在编译时间

捕获

更新

以某种方式,我错过了第695期的constexpr函数中的编译时间计算错误,这些函数是在5段的措辞上旋转的, 4 曾经说过(强调矿山将继续):

如果在表达式评估期间,该结果在数学上是否在数学上定义或不在其类型的代表值范围内,则行为是未定义的,,除非出现这样的表达式,而在需要一个积分常数表达式的情况下出现(5.19 [expr.const]),在这种情况下,程序不形成

继续说:

旨在作为"在编译时进行评估"的可接受的标准范围,该概念未直接由标准定义。目前尚不清楚该公式充分涵盖ConstexPr函数。

和后来的注释说:

[...]想要在编译时间诊断错误与不诊断在运行时实际不会发生的错误之间存在张力。[...] CWG的共识是像1/0这样的表达式应该简单地被认为是非恒定的;在需要恒定表达式的上下文中使用表达式的任何诊断都将导致。

如果我正确阅读,请确认目的是能够在需要恒定表达的情况下在编译时间诊断不确定的行为。

我们不能绝对说这是意图,但确实强烈暗示了。clanggcc处理未定义的班次的区别确实留出了一些空间。

我提交了GCC错误报告:左右移动不确定的行为不是constexpr中的错误。尽管这似乎是在符合的,但它确实打破了Sfinae,我们可以从我的回答中看到它是一个符合编译器扩展程序,可以将非constexpr标准库作为constexpr视为constexpr吗?Sfinae用户可以观察到的实施差异似乎是委员会不希望的。

当我们谈论不确定的行为时,重要的是要记住,标准为这些情况留下了不确定的行为。它不禁止实施更强大的保证。例如,某些实现可能保证签名的整数溢出包裹,而另一些则可以保证饱和。

要求编译器处理涉及未定义行为的常数表达式将限制实施可以做出的保证,从而限制它们在没有副作用的情况下产生一些价值(标准称为不确定的值)。这不包括在现实世界中发现的许多扩展保证。

例如,某些实现或伴随标准(即POSIX)可以定义零积分划分的行为以生成信号。这是一个副作用,如果表达式在编译时计算出来。

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

还有另一个要点是,从常数表达式中排除不确定的行为:根据定义,应在编译时编译器评估常数表达式。允许恒定的表达调用不确定的行为将使编译器本身显示出不确定的行为。以及一个格式化您的硬盘驱动器的编译器,因为您 compile 某些邪恶代码不是您想要的。