【问题标题】:Bit-shifting left and discarding bits向左移位和丢弃位
【发布时间】:2016-04-28 20:55:08
【问题描述】:

让我们考虑将无符号短值(或任何其他无符号整数类型)的右 N 位清零的函数(它的可能实现之一)。可能的实现如下所示:

template<unsigned int shift>
unsigned short zero_right(unsigned short arg) {
  using type = unsigned short;

  constexpr type mask = ~(type(0));
  constexpr type right_zeros = mask << shift; // <-- error here
  return arg & right_zeros;
}

int check() {
  return zero_right<4>(16);
}

使用此代码,我可以访问的所有编译器都以一种或另一种方式抱怨可能的溢出。 CLang 是最明确的,有以下明确的信息:

错误:从 'int' 到 'const type' 的隐式转换(又名 'const unsigned short') 将值从 1048560 更改为 65520 [-Werror,-Wconstant-conversion]

这段代码在我看来定义明确且清晰,但是当 3 个编译器抱怨时,我变得非常紧张。我在这里错过了什么吗?真的有可能发生可疑的事情吗?

附:虽然 zeriong out left X bits 的替代实现可能会受到欢迎和有趣,但这个问题的主要焦点是发布的代码的有效性。

【问题讨论】:

  • @TavianBarnes,对于无符号参数,它们不能提升为有符号整数。
  • 不是您要问的,但您可能需要注意(并且不要提防)的是,如果您将无符号整数左移 'n' 位,其中 'n' 是>= 你转换的类型的位数,那就是未定义的行为。
  • @SergeyA 但这仍然是问题所在:&lt;&lt; 的结果是 int,而不是短。在赋值之前将mask &lt;&lt; shift 的结果转换回type 会使错误消失。
  • @JesperJuhl,我已经解决了这个问题,只是不想让问题变得过于复杂。
  • @SergeyA cppreference 还说“返回类型是左操作数的类型在积分提升之后。”(强调我的)

标签: c++ language-lawyer bit-shift integer-overflow


【解决方案1】:

是的,正如您所怀疑的那样,即使在抑制编译器诊断之后,您的代码严格来说也不是完全可移植的,因为从 unsigned short 提升到有符号 int,位算术在有符号 int 中完成,然后有符号 int 被转换回为无符号短。您已经设法避免了未定义的行为(我认为,在快速浏览之后),但结果不能保证是您所希望的。 (type)~(type)0 不需要对应type 类型中的“所有位为一”;换班前就已经很不确定了。

要获得完全可移植的东西,只需确保至少在 unsigned int 中进行所有算术运算(如果需要,可以使用更宽的类型,但永远不要更窄)。这样就不用担心签名类型的任何促销活动了。

template<unsigned int shift>
unsigned short zero_right(unsigned short arg) {
  using type = unsigned short;

  constexpr auto mask = ~(type(0) + 0U);
  constexpr auto right_zeros = mask << shift;
  return arg & right_zeros;
}

int check() {
  return zero_right<4>(16);
}

【讨论】:

    【解决方案2】:

    来自 C++11 标准:

    5.8 移位运算符 [expr.shift]

    1 ...

    操作数应为整数或非范围枚举类型,并执行整数提升。结果的类型是提升的左操作数的类型。

    表达式

    mask << shift;
    

    在对mask 应用积分提升后评估。因此,如果 sizeof(unsigned short) 为 2,则计算结果为 1048560,这解释了来自 clang 的消息。

    避免溢出问题的一种方法是在执行左移之前先右移,然后将其移动到它自己的函数中。

    template <typename T, unsigned int shift>
    constexpr T right_zero_bits()
    {
       // ~(T(0)) performs integral promotion, if needed
       // T(~(T(0))) truncates the number to T, if needed.
       return (T(~(T(0))) >> shift ) << shift;
    }
    
    template<unsigned int shift>
    unsigned short zero_right(unsigned short arg) {
       return arg & right_zero_bits<unsigned short, shift>();
    }
    

    【讨论】:

    • 是的,它解释了消息,但没有解释警告:) 可能是我的问题不够清楚。问题是:我应该担心吗? :)
    • @SergeyA 警告与unsigned short mask = 1048560; 的警告相同,即不,您不应该担心,但您应该使用显式强制转换来抑制它。
    • @TavianBarnes,我很想相信你——但要成为一个合适的 LanguageLawyer 答案,它需要一些东西来证实这一说法:)
    • @SergeyA 哈哈,这就是我评论的原因,而不是回答:)。但是这里有一个来源的答案可以证实我:stackoverflow.com/a/6752688/502399
    • 第一个导致unsigned short的UB:~T(0)int 因为整数提升,它有负值; &gt;&gt; 可能会离开,填充然后 &lt;&lt; 导致 UB(有符号整数的左移)
    【解决方案3】:

    消息看起来很简单:

    错误:从“int”到“const type”的隐式转换(又名“const unsigned short”)将值从 1048560 更改为 65520 [-Werror,-Wconstant-conversion]

    mask &lt;&lt; shift 具有值1048560(源自65535 &lt;&lt; 4),并将其分配给unsigned short,它被定义为调整值mod 65536,得到65520

    最后一次转换是明确定义的。错误消息是因为您传递了编译器标志-Werror,-Wconstant-conversion 请求在这种情况下获取错误消息。如果您不希望出现此错误,请不要传递这些标志。


    虽然这个特定的用法是明确定义的,但对于某些输入可能存在未定义的行为(即,shift16 或更大,如果您在 32 位 int 系统上)。所以你应该修复这个功能。

    要修复该函数,您需要在 unsigned short 情况下更加小心,因为关于将 unsigned short 整数提升为有符号 int 的非常烦人的规则。

    这是一种与其他产品略有不同的解决方案。完全避免轮班问题,适用于任何班次大小:

    template<unsigned int shift, typename T>
    constexpr T zero_right(T arg)
    {
        T mask = -1;
        for (int s = shift; s--; ) mask *= 2u;
        return mask & arg;
    }
    
    // Demo
    auto f() { return zero_right<15>((unsigned short)65535); }  //  mov eax, 32768
    

    【讨论】:

    • 这很有趣。我意识到我不能移动更多的位然后有类型。在我的实际应用程序中,这不会发生。除此之外,您是说代码定义明确,并且总是按照我的预期去做?
    • 你现在拥有它的方式,依赖于 2 的补码,如果你有 32 位整数,无符号短大小写是实现定义的,用于移位 15
    • @SergeyA,您可以尝试移位多于宽度的位,但英特尔明确表示它们会屏蔽 shift 操作数的高位。例如,对于 uint32_t,移位是 s % 32,所以 int32_t &lt;&lt; 40 === int32_t &lt;&lt; 8但是,当心编译器 - 如果 gcc 在(优化的)编译时看到 shift > 32,它只会将结果归零!
    【解决方案4】:

    我不知道这是否正是你想要的,但它可以编译:

    template<unsigned int shift>
    unsigned short zero_right(unsigned short arg) {
      using type = unsigned short;
    
      //constexpr type mask = ~(type(0));
      type right_zeros = ~(type(0));
      right_zeros <<= shift;
      return arg & right_zeros;
    }
    
    int check() {
      return zero_right<4>(16);
    }
    

    更新:

    似乎您只是通过确保编译器不知道类型发生了什么来让编译器安静下来。

    没有

    首先你会得到right_zeros,其值为FFFF(来自~0)。通常,~0FFFFFFFFFFFFFF...,但因为您使用的是 u16,所以您会得到 FFFF

    然后,移位4产生FFFF0[计算扩展为32位],但是存回时,只剩下最右边的16位,所以值为FFF0

    这是完全合法且已定义的行为,您正在利用截断。编译器不是“被愚弄”。实际上,无论有没有截断,它都可以正常工作。

    如果您愿意,您可以将right_zeros 设置为 u32 或 u64,但是您需要添加 right_zeros &amp;= 0xFFFF

    如果存在未定义的行为(我的问题的本质!),您只是让它无法检测到。

    无论编译器怎么说,没有 UB 基于你的代码的整体性。

    实际上,Tavian 明白了。使用显式转换:

    constexpr type right_zeros = (type) (mask << shift); // now clean
    

    这告诉编译器,除其他外,您希望将截断为 16 位。

    如果有 UB,那么编译器仍然应该抱怨。

    【讨论】:

    • 似乎您只是通过确保编译器不知道类型发生了什么来使编译器安静下来。如果存在未定义的行为(我的问题的本质!),您只需让它无法检测到。
    • "如果有 UB,那么编译器仍然应该抱怨。" - 不要指望它
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多