位左移和丢弃位操作

6

让我们考虑一个函数(可能的实现之一),它将无符号短整型值(或任何其他无符号整型)的右侧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个编译器抱怨时,我变得非常紧张。 我是否错过了什么? 真的有可能发生一些可疑的事情吗?
附注:虽然对左侧X位进行零化的替代实现可能是受欢迎和有趣的,但这个问题的主要重点是发布的代码的有效性。

不是你所询问的,但你可能需要知道的一件事(并且不要忽略)是,如果你将一个无符号整数左移'n'位,其中'n'>=你所移动的类型的位数,则这是未定义行为。 - Jesper Juhl
@SergeyA 但这仍然是问题:<< 的结果是 int,而不是 short。在赋值之前将 mask << shift 的结果转换回 type 可以消除错误。 - Mr Lister
在 ideone 上没有警告。 - zdf
@zdf,提高警告级别:D - SergeyA
1
@SergeyA cppreference 还说:“返回类型是整数提升后左操作数的类型。”(强调是我的) - Mr Lister
显示剩余6条评论
4个回答

3

来自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; 相同,也就是说你不必担心,但最好使用显式转换来抑制它。 - Tavian Barnes
@TavianBarnes,我很想相信你 - 但为了成为一个合适的“语言律师”回答,它需要一些证明来支持这个说法 :) - SergeyA
@SergeyA 哈哈,这就是为什么我在评论而不是回答:)。但是这里有一个来源的答案证实了我的说法:https://dev59.com/QGw15IYBdhLWcg3wO5IH#6752688 - Tavian Barnes
@M.M,你是对的。我认为更新后的答案是正确的。 - R Sahu
更新后仍将“1”移入符号位,尽管自C++14以来这是实现定义的,但很可能会起作用。 - M.M
显示剩余2条评论

3
是的,正如你所怀疑的那样,即使在抑制编译器诊断之后,由于从无符号短整型到有符号整型的提升、位运算在有符号整型中进行,然后将有符号整型转换回无符号短整型,你的代码严格来说仍然不是完全可移植的。你已经成功地避免了未定义的行为(我认为,在快速查看之后),但结果并不能保证是你所希望的。在类型type中,“(type)~(type)0”不需要对应于“全部位为1”的情况;在进行移位操作之前,这就已经很棘手了。
要获得完全可移植的东西,只需确保所有算术运算都至少在无符号整型中进行(如果必要,可以使用更宽的类型,但永远不要使用更狭窄的类型)。然后就不会有任何提升到有符号类型需要担心的问题。
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

我不知道这是否完全符合您的要求,但它可以编译:

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);
}

更新:

看起来你仅仅通过让编译器不知道类型的情况来压制它。

不是这样的。

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

然后,将其左移 4 位会产生 FFFF0 [计算扩展到 32 位],但当存回时,只有最右边的 16 位保留下来,所以值为 FFF0

这是完全合法和定义明确的行为,你正在利用这种截断。编译器并没有被“愚弄”。实际上,它可以正常工作,无论是否进行截断。

如果你希望,可以将 right_zeros 改成 u32 或 u64,但那么你需要添加 right_zeros &= 0xFFFF

如果存在未定义的行为(这正是我的问题的本质!),你只是让它变得不可检测。

根据你的全部代码,不存在任何未定义行为,无论编译器说什么。

实际上,Tavian 已经解决了这个问题。使用显式转换:

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

这是在告诉编译器,你想要截断为16位,同时还有其他信息。

如果存在未定义行为(UB),编译器仍应该报错。


1
看起来你只是通过确保编译器不知道类型的情况来压制了它。如果存在未定义的行为(这正是我的问题的本质!),那么你只是使其无法被检测到。 - SergeyA
3
如果存在UB(未定义行为),那么编译器仍应该发出警告。 - 不要期望这样的情况会发生。 - M.M

2
这条消息似乎很明显:
错误:从“int”到“const type”(即“const unsigned short”)的隐式转换会将值从1048560更改为65520 [-Werror,-Wconstant-conversion] mask << shift的值为1048560(源自65535 << 4),然后您将其分配给了unsigned short,它被定义为调整值mod 65536,得到65520
最后一个转换是良好定义的。错误消息是因为您传递了编译器标志-Werror,-Wconstant-conversion,请求在这种情况下获得错误消息。如果您不想要此错误,则不要传递这些标志。
尽管这个特定用法是良好定义的,但对于某些输入可能存在未定义行为(即,在32位int系统上shift大于或等于16)。因此,您应该修复该函数。
要修复该函数,您需要在unsigned short情况下更加小心,因为有关无符号短整数升级为带符号整数的极其恼人的规则。
这里是与其他方案略有不同的解决方案..完全避免了移位问题,适用于任何移位大小:
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

有趣。我意识到我不能移动比类型中存在的位数更多的位。在我的实际应用中,这不会发生。除此之外,您是说代码是明确定义的,并且始终会按照我期望的方式执行? - SergeyA
1
你现在的做法依赖于二进制补码,如果你有32位整数并且进行了15位移位,则无符号短整型情况下的实现定义未确定。 - M.M
@SergeyA,你可以尝试移位比宽度更多的位,但是英特尔明确表示他们会掩盖shift操作数的高位。例如,对于uint32_t,移位是“s % 32”,因此“int32_t << 40” === “int32_t << 8”。但是,请注意编译器 - 如果gcc在(优化后)编译时看到移位 > 32,它将只清零结果! - BitWhistler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接