执行无符号<->有符号转换的正确方法

Proper way to perform unsigned<->signed conversion

上下文

我有一个 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 类型上。

问题

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

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

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

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

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

我找不到任何明确说明它是或不是未定义行为的东西,我只是发现这个 Microsoft documentation 他们在其中使用隐式转换进行操作,而没有提及未定义行为。


这里举个例子来说明:

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)(B)(分别是(A') (B')) 如果值在目标类型中不可表示,则为未定义行为 ?

(C) (resp. (C')) 定义明确吗?

I know that signed types overflow is undefined behaviour,

正确,但不适用于此处。

a += 140; 不是 有符号整数溢出,不是 UB。这就像 a = a + 140; a + 140a 是 8 位 signed charunsigned[ 时不会溢出=59=] char.

问题是当总和 a + 140 超出 char 范围并分配给 char 时会发生什么。

Otherwise, the new type is signed and the value cannot be represented in it; either the result is implementation-defined or an implementation-defined signal is raised. 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 并避免这一切。 unsigned char 定义为换行。

为了完全的可移植性,您确实遇到了一个小问题,因为(char1 除外)签名数据类型尚未2 需要与未签名的对应值一样多的不同值。很少有系统实际使用 sign-magnitude 表示整数类型,但如果你不能排除它们,那么简单地在无符号对应物中进行数学运算实际上并不能保证 round-tripping,即使你使用 numeric_limits<?>::min() 尽量避免转换无法表示的值。

有了这个警告,对你的问题的直接回答是 隐式转换和 static_cast 对于在有符号和无符号之间转换值都是正确的(并且等价的)对应类型。在 signed->unsigned 方向,行为是 well-defined 标准,而在另一个方向,行为是 implementation-defined.


1 charsigned char 本身通过支持访问任何对象的字节表示,包括 unsigned 要求不存在任何缺失值的对象。

2 最新版本的 C++ 需要二进制补码转换行为,请参阅 https://eel.is/c++draft/basic.fundamental#3