代码之家  ›  专栏  ›  技术社区  ›  Prasoon Saurav

当左侧操作数为负值时,为什么左移位操作调用未定义的行为?

  •  40
  • Prasoon Saurav  · 技术社区  · 15 年前

    ISO C99(6.5.7/4)的相关引用

    ,约化模 比结果类型中可表示的最大值多一个。如果E1有签名 E2级 在结果类型中是可表示的,那么 结果值;否则, .

    E1<<E2的值是E1(解释为位模式)左移的E2位位置;空出的位是零填充的。如果E1有一个无符号类型,结果的值是E1乘以2的次方E2,如果E1有无符号长类型,则减少模ULONG\u MAX+1,否则为UINT\u MAX+1。 [注:常数ULONG\u max和UINT\u max在标题中定义)。]

    这意味着

    int a = -1, b=2, c;
    c= a << b ;
    

    调用C中的未定义行为,但行为在C++中定义良好。

    是什么促使ISO C++委员会认为行为定义得好于C的行为?

    另一方面,这种行为是 implementation defined 对于左操作数为负时的按位右移操作,对吗?

    我的问题是为什么左移位操作调用C中未定义的行为,为什么右移位操作只调用实现定义的行为?

    注:请不要给出“这是未定义的行为,因为标准是这么说的”这样的答案。:页

    8 回复  |  直到 10 年前
        1
  •  35
  •   Pedro Gimeno    6 年前

    您复制的段落是关于未签名类型的。行为 C++中未定义的。从最后的C++ 0X草案:

    左移的E2位位置;空的 无符号类型,结果的值 E2级 ,又约化了一个模 大于可表示的最大值 有符号类型且非负 值和E12 E2级 是在 结果类型,即 否则 .

    编辑:看一看C++ 98的论文。它根本没有提到签名类型。所以这仍然是未定义的行为。

    右移负数是实现定义的,对吗。为什么?在我看来:它很容易实现定义,因为没有从左边的问题截断。当你左移时,你不仅要说从右移的是什么,还要说其余的位发生了什么,例如用2的补码表示,这是另一个故事。

        2
  •  18
  •   sellibitze    15 年前

    在C中,当 左侧操作数具有负值。 但是在C++中,行为是很好定义的。

    简单的答案是:因为标准是这么说的。

    一个较长的答案是:它可能与C和C++两个都允许2个余数的负数的其他表示有关。对将要发生的事情给予较少的保证使得在其他硬件上使用这些语言成为可能,包括晦涩难懂的和/或旧的机器。

    出于某种原因,C++标准化委员会想添加一点关于比特表示如何改变的保证。但是,由于负数仍然可以通过1的补码或符号+大小来表示,因此结果值的可能性仍然不同。

    假设16位整数

     -1 = 1111111111111111  // 2's complement
     -1 = 1111111111111110  // 1's complement
     -1 = 1000000000000001  // sign+magnitude
    

     -8 = 1111111111111000  // 2's complement
    -15 = 1111111111110000  // 1's complement
      8 = 0000000000001000  // sign+magnitude
    

    是什么迫使ISO C++委员会充分考虑了行为 定义为与C中的行为相反?

    另一方面,行为是按位定义的实现 左操作数为负时的右移位操作,对吗?

    我得检查一下标准。但你可能是对的。在2的补码机上没有符号扩展的右移不是特别有用。因此,当前状态肯定比要求空出的位为零填充要好,因为它为执行符号扩展的机器留下了空间——尽管不能保证。

        3
  •  7
  •   Jens Gustedt    9 年前

    回答标题中所述的真实问题:对于有符号类型上的任何操作,如果数学运算的结果不适合目标类型(under-或overflow),则这具有未定义的行为。有符号整数类型就是这样设计的。

    对于左移运算,如果值为正或0,则将运算符定义为2的幂的乘法是有意义的,因此一切正常,除非结果溢出,否则没有什么奇怪的。

    我的结论是:

    • 如果你想做真正的位模式
    • 如果你想乘以 值(无论是否有符号)的二次幂,只要 比如说

      i*(1u<<k)

    在任何情况下,你的编译器都会把它转换成像样的汇编程序。

        4
  •  4
  •   Tony Delroy    15 年前

    很多这类事情都是在普通cpu在一条指令中实际支持的内容和足够有用的内容之间的一种平衡,即使它需要额外的指令,编译器编写者也能保证这些内容。一般来说,使用位移位运算符的程序员希望它们映射到具有此类指令的CPU上的单个指令,所以这就是为什么存在未定义或实现行为,其中CPU对“边缘”条件有各种处理,而不是强制执行某个行为并使操作出人意料地慢。请记住,即使对于更简单的用例,也可以制定额外的预/后或处理说明。未定义的行为可能是必要的,在某些CPU产生陷阱/异常/中断(与C++尝试/捕获类型异常不同)或一般无用/无法解释的结果时,而如果标准委员会考虑的CPU集合在时间上都至少提供了一些定义的行为,然后他们可以定义行为实现。

        5
  •  1
  •   jww avp    11 年前

    LLVM的工作人员推测,由于指令在不同平台上的实现方式,移位运算符有限制。从 What Every C Programmer Should Know About Undefined Behavior #1/3 :

    ... 我猜这是因为不同CPU上的底层移位操作对此执行不同的操作:例如,X86将32位移位量截断为5位(因此32位移位与0位移位相同),而PowerPC将32位移位量截断为6位(因此32位移位产生零)。由于这些硬件的差异,行为完全没有定义的C。。。

    Nate说讨论的是移位量大于寄存器大小。但这是我发现的最接近于解释来自权威的转移约束的方法。

    第二个原因是2的恭维机器上潜在的符号变化。但我从未在任何地方读过它(无意冒犯@sellibitz(我碰巧同意他的看法)。

        6
  •  0
  •   Ben Voigt    11 年前

    C++ 03中的行为与C++ 11和C99中的行为相同,只需要查看规则以进行左移位。

    本标准第5p5节规定:

    如果在表达式求值期间,结果未在数学上定义或不在其类型的可表示值范围内,则行为未定义

    在C99和C++ 11中明确指出的左移位表达式是未定义行为,它们是对可表示值范围之外的结果进行评估的相同表达式。

        7
  •  0
  •   supercat    6 年前

    C89强制的行为对于没有填充的两个补码平台是有用和明智的,至少在将它们当作乘法处理不会导致溢出的情况下是这样。在其他平台上,或者在寻求可靠捕获有符号整数溢出的实现上,这种行为可能不是最佳的。C99的作者可能希望在C89规定的行为不太理想的情况下允许实现灵活性,但基本原理中没有任何内容表明,在没有令人信服的理由不这样做的情况下,高质量的实现不应继续以旧方式进行。

    不幸的是,尽管从来没有任何C99的实现不使用二的补码数学,C11的作者拒绝定义常见情况(非溢出)行为;IIRC,声称这样做会阻碍“优化”。如果左移位运算符在左操作数为负时调用未定义行为,则编译器可以假定只有在左操作数为非负时才可以访问移位。

    我对这种优化真正有用的频率表示怀疑,但这种有用性的稀有性实际上有利于保持行为的不确定性。如果两个s-complement实现不会以普通方式运行的唯一情况是那些优化实际有用的情况,并且如果实际上不存在这样的情况,那么实现将以普通方式运行(有或没有授权),并且没有必要授权该行为。

        8
  •  -2
  •   Reality Pixels    9 年前

    1001 如果我们把这个移位,我们会得到 这是-6的表示。类似地,-1表示为-1+8 左移时,表示+6。位行为是定义良好的,但数值行为高度依赖于表示系统。