执行无符号<>签名转换的正确方法



上下文

我有一个char变量,需要对其应用转换(例如,添加偏移量)。转换的结果可能溢出,也可能不溢出
执行转换后,我并不真正关心变量的实际值
我想要的唯一保证是,如果我以相反的方式再次执行转换(例如,减去偏移量),我必须能够检索原始值。

基本上:

char a = 42;
a += 140; // overflows (undefined behaviour)
a -= 140; // must be equal to 42

问题

我知道signed类型的溢出是未定义的行为,但unsigned类型的溢出并非如此。然后,我选择在执行转换的过程中添加一个中间步骤。

然后它将变成:

  1. char->unsigned char转换
  2. 应用变换(对应反向变换)
  3. unsigned char->char转换

这样,我可以保证潜在的溢出只会发生在unsigned类型中。

问题

我的问题是,进行这种转换的正确方式是什么?

我想到了三种可能性。我可以:

  • 隐式转换
  • static_cast
  • reinterpret_cast

哪一个是有效的(不是未定义的行为)?我应该使用哪一个(正确的行为)?

我的猜测是,我需要使用reinterpret_cast,因为我不在乎实际值,我想要的唯一保证是内存中的值保持不变(即位不变),这样它就可以可逆。

另一方面,我不确定在值不能在目标类型中表示(超出范围)的情况下,隐式转换或static_cast是否不会触发未定义的行为。

我找不到任何明确说明它是或不是未定义行为的东西,我只是找到了这篇微软文档,他们在文档中使用了隐式转换,而没有提及未定义行为。


以下是一个示例,用于说明:

char a = -4;                                             // out of unsigned char range
unsigned char b1 = a;                                    // (A)
unsigned char b2 = static_cast<unsigned char>(a);        // (B)
unsigned char b3 = reinterpret_cast<unsigned char&>(a);  // (C)
std::cout << (b1 == b2 && b2 == b3) << 'n';
unsigned char c = 252;                                   // out of (signed) char range
char d1 = c;                                             // (A')
char d2 = static_cast<char>(c);                          // (B')
char d3 = reinterpret_cast<char&>(c);                    // (C')
std::cout << (d1 == d2 && d2 == d3) << 'n';

输出为:

true
true

除非触发未定义的行为,否则这三种方法似乎都能工作。

如果目标类型中的值不可表示,(A)

(C)(分别为

我知道签名类型溢出是未定义的行为,

为True,但不适用于此处。

a += 140;而不是有符号整数溢出,而不是UB。这类似于当a是8位有符号char无符号char时,a = a + 140;a + 140不会溢出。

问题是当总和a + 140char范围之外并且被分配给char时会发生什么。

否则,将对新类型进行签名,并且无法在其中表示值;要么结果是实现定义的,要么产生实现定义的信号。C17dr§6.3.1.3 3

这是实现定义的行为,当char符号和8位时-分配char范围之外的值。

通常实现定义的行为是一个包装,并且是完全定义的,所以a += 140;是可以的。

或者,实现定义的行为可能是在char签名时将值限制在char范围内。

char a = 42;
a += 140;
// Might act as if
a = max(min(a + 140, CHAR_MAX), CHAR_MIN);
a = 127;   

为了避免实现定义的行为,在作为unsigned char访问的a上执行+-

*((unsigned char *)&a) += small_offset;

或者只使用unsigned char a并避免所有这些。CCD_ 32被定义为包装。

为了实现完全可移植性,您确实遇到了一个小问题,因为(除了char1)有符号的数据类型没有被2要求具有与其无符号对应的值一样多的不同值。很少有系统真正使用符号-幅度表示来表示积分类型,但如果不能排除它们,那么简单地在无符号对应物中进行数学运算实际上并不能保证往返,即使您使用numeric_limits<?>::min()来避免转换不可表示的值。

有了这个警告,问题的直接答案是隐式转换和static_cast对于在有符号和无符号对应类型之间转换值是正确的(也是等效的)。在签名的->无符号方向,行为由标准定义,而在另一个方向,行为是由实现定义的。


1charsigned char本身通过其对访问任何对象的字节表示的认可而免于这种可能性,包括对要求不具有任何缺失值的unsigned对象的认可。

2Two的补码转换行为在最新版本的C++中是必需的,请参阅https://eel.is/c++草稿/基本。基本#3

最新更新