执行无符号<->有符号转换的正确方法
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
类型溢出不是这种情况。然后我选择在过程中添加一个中间步骤来执行转换。
它将变成:
char
-> unsigned char
转换
- 应用转换(相应的反向转换)
unsigned char
-> char
转换
这样,我可以保证潜在的溢出只会发生在 unsigned
类型上。
问题
我的问题是,执行这种转换的正确方法是什么?
我想到了三种可能。我可以:
- 隐式转换
static_cast
reinterpret_cast
哪一个是有效的(不是未定义的行为)?我应该使用哪一个(正确的行为)?
我的猜测是我需要使用 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 + 140
在 a
是 8 位 signed char
或 unsigned[ 时不会溢出=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
定义为换行。
为了完全的可移植性,您确实遇到了一个小问题,因为(char
1 除外)签名数据类型尚未2 需要与未签名的对应值一样多的不同值。很少有系统实际使用 sign-magnitude 表示整数类型,但如果你不能排除它们,那么简单地在无符号对应物中进行数学运算实际上并不能保证 round-tripping,即使你使用 numeric_limits<?>::min()
尽量避免转换无法表示的值。
有了这个警告,对你的问题的直接回答是 隐式转换和 static_cast
对于在有符号和无符号之间转换值都是正确的(并且等价的)对应类型。在 signed->unsigned 方向,行为是 well-defined 标准,而在另一个方向,行为是 implementation-defined.
1 char
和 signed char
本身通过支持访问任何对象的字节表示,包括 unsigned
要求不存在任何缺失值的对象。
2 最新版本的 C++ 需要二进制补码转换行为,请参阅 https://eel.is/c++draft/basic.fundamental#3
上下文
我有一个 char
变量,我需要对其应用转换(例如,添加偏移量)。转换的结果可能溢出也可能不溢出。
我不太关心执行转换后变量的实际值。
我想要的唯一保证是,如果我以相反的方式再次执行转换(例如,减去偏移量),我必须能够检索到原始值。
基本上:
char a = 42;
a += 140; // overflows (undefined behaviour)
a -= 140; // must be equal to 42
问题
我知道 signed
类型溢出是未定义的行为,但 unsigned
类型溢出不是这种情况。然后我选择在过程中添加一个中间步骤来执行转换。
它将变成:
char
->unsigned char
转换- 应用转换(相应的反向转换)
unsigned char
->char
转换
这样,我可以保证潜在的溢出只会发生在 unsigned
类型上。
问题
我的问题是,执行这种转换的正确方法是什么?
我想到了三种可能。我可以:
- 隐式转换
static_cast
reinterpret_cast
哪一个是有效的(不是未定义的行为)?我应该使用哪一个(正确的行为)?
我的猜测是我需要使用 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 + 140
在 a
是 8 位 signed char
或 unsigned[ 时不会溢出=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
定义为换行。
为了完全的可移植性,您确实遇到了一个小问题,因为(char
1 除外)签名数据类型尚未2 需要与未签名的对应值一样多的不同值。很少有系统实际使用 sign-magnitude 表示整数类型,但如果你不能排除它们,那么简单地在无符号对应物中进行数学运算实际上并不能保证 round-tripping,即使你使用 numeric_limits<?>::min()
尽量避免转换无法表示的值。
有了这个警告,对你的问题的直接回答是 隐式转换和 static_cast
对于在有符号和无符号之间转换值都是正确的(并且等价的)对应类型。在 signed->unsigned 方向,行为是 well-defined 标准,而在另一个方向,行为是 implementation-defined.
1 char
和 signed char
本身通过支持访问任何对象的字节表示,包括 unsigned
要求不存在任何缺失值的对象。
2 最新版本的 C++ 需要二进制补码转换行为,请参阅 https://eel.is/c++draft/basic.fundamental#3