左移位和丢弃位

时间:2016-04-28 20:55:08

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

让我们考虑函数(它的可能实现之一),它将无符号短值(或任何其他无符号整数类型)的右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是最明确的一个,有以下明确的信息:

  

错误:来自&#39; int&#39;的隐式转换到'const类型&#39; (又名&#39; const   unsigned short&#39;)将值从1048560更改为65520   [-Werror,-Wconstant转换]

这段代码看起来很清晰,对我来说很明显,但是当3个编译器抱怨时,我变得非常紧张。我在这里错过了什么吗?真的有可能发生腥病吗?

P.S。虽然zeriong out左边X位的替代实现可能是受欢迎和有趣的,但这个问题的主要焦点是发布的代码的有效性。

4 个答案:

答案 0 :(得分:3)

来自C ++ 11标准:

  

5.8移位运算符[expr.shift]

     

1   ...

     

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

表达式

mask << shift;

在积分促销后评估为mask。因此,如果1048560为2,则评估为sizeof(unsigned short),这解释了来自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>();
}

答案 1 :(得分:3)

是的,正如您所怀疑的那样,即使在抑制编译器诊断之后,您的代码严格来说也不是完全可移植的,因为从unsigned short到signed int的升级,在signed int中进行位运算,然后将signed 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 :(得分: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);
}

<强>更新

  

好像你只是通过确保它不知道这些类型发生了什么来简单地放弃编译器。

没有

首先,您获得值为right_zeros的{​​{1}}(来自FFFF)。通常情况下,~0~0,但由于您使用的是FFFFFFFFFFFFFF...,因此您会获得u16

然后,移位4会产生FFFF [计算扩展到32位],但是当存储回来时,只剩下最右边的16位,因此值为FFFF0

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

如果您愿意,可以将FFF0设为u32或u64,但之后您需要添加right_zeros

  

如果存在未定义的行为(我的问题的本质!),你只是让它无法察觉。

无论编译器说什么,基于代码的总体都有 no UB。

实际上,塔维安得到了它。使用显式强制转换:

right_zeros &= 0xFFFF

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

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

答案 3 :(得分:2)

这个消息很明显:

  

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

procedure TForm1.Button4Click(Sender: TObject); var Center:TPoint; Radius:integer; Bmp: TBitmap; MF: TMetaFile; MetafileCanvas: TMetafileCanvas; begin MF := TMetaFile.Create; MF.Width := 150; MF.Height := 150; MetafileCanvas := TMetafileCanvas.Create(MF, 0); MetafileCanvas.Pen.Color := clRed; MetafileCanvas.Brush.Color := clRed; MetafileCanvas.Brush.Style := bsSolid; MetafileCanvas.Pie(0,0,150, 150, 150,50, 150,50); MetafileCanvas.Free; Bmp := Gdip.DrawAntiAliased(MF); Image2.Picture.Assign(Bmp); Bmp.Free; MF.Free; end; 的值为mask << shift(来自1048560),您将其分配给65535 << 4,其定义为调整值unsigned shortmod 65536

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

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

要修复这个函数,你需要在16的情况下更加小心,因为关于unsigned short到signed int的整数提升这个非常烦人的规则。

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

unsigned short