是(int32_t)255 << 24 gcc(C ++ 11)中未定义的行为吗?

时间:2018-12-17 10:45:47

标签: c++ c++11 language-lawyer undefined-behavior bit-shift

在C ++ 11中,根据en.cppreference.com

  

对于有符号和非负a,<< <<的值是    a * 2 b (如果可以用返回类型表示),   否则行为是不确定的。

我的理解是,由于255 * 2 24 不是 可表示为int32_t的{​​{1}}的求值 产生未定义的行为。那是对的吗?可以吗 依赖于编译器?如果重要的话,这是IP16环境。

背景:来自an argument I am having,带有一个 arduino.stackexchange.com上的用户。据他说,“什么都没有 完全没有定义”:

  

您会注意到,很多位移是“实现定义的”。   因此,您不能在规格中引用章节。你得走了   到GCC文档,因为那是唯一可以说明的地方   你实际发生了什么。   gnu.org/software/gnu-c-manual/gnu-c-manual.html#Bit-Shifting-   负移位值只是“未定义”。


编辑:从目前的答案来看,我似乎对 C ++ 11标准是正确的。那么我的问题的关键是 该表达式调用 gcc 中的未定义行为。正如davmac所说的 在他的评论中,我问“ GCC是否是一种实现, 行为,即使语言标准未对其进行定义也是如此。”

从我链接到的gcc手册看来,它确实已经定义好了, 尽管我发现本手册的措词听起来更像是教程 而不是“语言法”。摘自PSkocik的回答(以及Kane对此的评论 答案),似乎它是未定义的。所以我还是有疑问。

我想我的梦想是在一些海湾合作委员会中发表明确的声明 说明以下内容的文档:1)gcc没有定义任何行为 在标准中明确未定义,或者2)gcc确实定义了 XX.XX版中的行为,并承诺保持所有定义 后续版本。

编辑2 :PSkocik删除了他的答案,我觉得很不幸,因为 它提供了有趣的信息。从他的回答中,凯恩的评论 答案和我自己的实验:

  1. (int32_t) 255 << 24使用clang编译时会产生运行时错误 和(int32_t)255<<24
  2. 即使使用g ++,同一代码也不会产生错误 -fsanitize=undefined
  3. -fsanitize=undefined编译时确实给出了运行时错误 (int32_t)256<<24

第2点与gcc在C ++ 11模式下的解释一致, 比标准更广泛地定义左移。根据第3点, 这个定义可能只是C ++ 14定义。但是,第3点是 不一致the referenced manual是一个 该手册提供了gcc(C ++ 11模式)中g++ -std=c++11 -fsanitize=undefined的完整定义 没有提示<<可能不确定。

4 个答案:

答案 0 :(得分:8)

随着时间的推移,这种情况发生了变化,这是有充分理由的,因此让我们回顾一下历史。请注意,在所有情况下,简单地定义static_cast<int>(255u << 24)都是行为。也许只是这样做并回避所有问题。


C++11的原始措辞是:

  

E1 << E2的值是E1个左移E2位的位置;空出的位为零。如果E1具有无符号类型,则结果的值为E1×2E2,与结果类型中可表示的最大值相比,模减少了1。否则,如果E1具有带符号的类型且非负值,并且E1×2E2在结果类型中可表示,则为结果值;否则,行为是不确定的。

255 << 24在C ++ 11中是未定义的行为,因为结果值无法表示为32位带符号整数,它太大。

此未定义行为会引起一些问题,因为constexpr 必须诊断未定义行为-因此,一些常见的设置值方法会导致严重错误。因此,CWG 1457

  

当前8.8 [expr.shift]第2段的措词使通过将(带符号的)1左移到符号位来创建给定类型的最负整数成为不确定的行为,即使这种情况并不罕见daccess-ods.un.org daccess-ods.un.org因此,该技术无法在大多数(二进制补码)体系结构上正常工作。因此,该技术不能在常量表达式中使用,否则将破坏大量代码。

这是针对C ++ 11的缺陷。从技术上讲,一个合格的C ++ 11编译器将实现所有缺陷报告,因此可以正确地说,在C ++ 11中,这是 not 未定义的行为。在C ++ 11中255 << 24的行为被定义为-16777216

缺陷后的措辞可以在C++14中看到:

  

E1 << E2的值是E1个左移E2位的位置;空出的位为零。如果E1具有无符号类型,则结果的值为E1×2E2,与结果类型中可表示的最大值相比,模减少了1。否则,如果E1具有带符号的类型且非负值,并且E1×2E2可表示为结果类型的相应的无符号类型,则该值,转换为结果类型是结果值;否则,行为是不确定的。

C ++ 17中的措词/行为没有变化。

但是对于C ++ 20,由于Signed Integers are Two's Complement(及其wording paper)的结果,措辞为greatly simplified

  

E1 << E2的值是E1×2E22N的唯一值,其中N是结果类型的范围指数。

255 << 24在C ++ 20中仍然定义了行为(具有相同的结果值),只是关于如何到达那里的规范变得更加简单,因为该语言不必围绕有符号整数的表示形式是实现定义的事实。

答案 1 :(得分:3)

“未定义行为是标准没有要求的行为。”这意味着它甚至可能是预期/正确的行为。 C ++标准规定了移位运算符。

  

8.5.7 Shift运算符[expr.shift] (C ++标准草案N4713)

     
      
  1. 操作数应为整数或无范围枚举类型,并执行整数提升。结果的类型是提升后的左操作数的类型。如果右操作数为负,或者大于或等于提升后的左操作数的位长度,则该行为未定义。否则,如果E1具有带符号的类型且非负值,并且E1×2E2在结果类型的相应无符号类型中可表示,则该值(转换为结果类型)就是结果值;否则,行为是不确定的。
  2.   

正如@rustyx在下面指出的那样,“措词“ E1×2E2在结果类型的相应无符号类型中是可表示的”是C++14。不幸的是,C++11中的UB仍然是。”

答案 2 :(得分:3)

GNU C 编译器按照manual中的描述完全定义了左/右移位:

  

GCC仅支持两种补码整数类型,并且所有位模式均为普通值。

     

。 。

     

作为C语言的扩展,GCC并不使用C99和C11中给出的纬度仅将带符号的“ <<”的某些方面视为未定义。但是,-fsanitize=shift(和-fsanitize=undefined)将诊断这种情况。它们还会被诊断为需要常量表达式的地方。

因此,这与您的发现相符-代码达到了您的期望,仅将诊断超出可用位(包括符号位)的溢出。

至于GNU C ++ 编译器,该文档似乎确实是lacking。我们只能猜测,尽管至少sanitizer似乎意识到了语言上的差异,但是尽管忽略了这一转变,但它在G ++中的工作方式与在GCC中的工作方式相同:

  

-fsanitize=shift

     

此选项使您可以检查移位操作的结果是否未定义。请注意,在C和C ++之间以及在ISO C90和C99等之间,确切地认为未定义的内容略有不同。

答案 3 :(得分:0)

对于C ++ 14之前的C ++编译器,如果您具有这样的功能:

// check if the input is still positive,
// after a suspicious shift

bool test(int input)
{
    if (input > 0) 
    {
        input = input << 24;

        // how can this become negative in C++11,
        // unless you are relying on UB?

        return (input > 0); 
    }
    else 
    {
        return false;
    }
}

然后将允许一个优化的编译器将其更改为此:

bool test(int input)
{
    // unless you are relying on UB, 
    // input << 24 must fit into an int,
    // and input is positive in that branch

    return (input > 0);
}

每个人都很高兴,因为您在发行版本中获得了不错的加速。

但是,我不知道实际上对左移进行了此类优化的编译器,尽管将附加内容优化为seen in this example也是很普遍的。