假设您正在使用 <cstdint>
和类型,如 std::uint8_t
和 std::uint16_t
,并希望对它们执行像 +=
和 *=
这样的操作。 您希望这些数字上的算术运算可以模块化地环绕,就像在 C/C++ 中一样典型。通常情况下,这是有效的,并且通过实验发现适用于 std::uint8_t
、std::uint32_t
和 std::uint64_t
,但不适用于 std::uint16_t
。
具体而言,使用 std::uint16_t
的乘法有时会造成失败,优化后的构建会产生各种奇怪的结果。 原因是由于有符号整数溢出而导致的未定义行为。编译器基于未发生未定义行为的假设进行优化,因此开始从程序中剪枝代码块。具体的未定义行为如下:
std::uint16_t x = UINT16_C(0xFFFF);
x *= x;
原因是C++的提升规则以及您(和几乎每个人一样)正在使用一个平台,其中std::numeric_limits<int>::digits == 31
,也就是说,int
为32位(digits
计算位数而不包括符号位)。尽管是无符号的,x
被提升为signed int
,并且0xFFFF * 0xFFFF
在32位有符号算术中溢出。
一般问题的演示:
// Compile on a recent version of clang and run it:
// clang++ -std=c++11 -O3 -Wall -fsanitize=undefined stdint16.cpp -o stdint16
#include <cinttypes>
#include <cstdint>
#include <cstdio>
int main()
{
std::uint8_t a = UINT8_MAX; a *= a; // OK
std::uint16_t b = UINT16_MAX; b *= b; // undefined!
std::uint32_t c = UINT32_MAX; c *= c; // OK
std::uint64_t d = UINT64_MAX; d *= d; // OK
std::printf("%02" PRIX8 " %04" PRIX16 " %08" PRIX32 " %016" PRIX64 "\n",
a, b, c, d);
return 0;
}
你将会收到一个友好的错误提示:
main.cpp:11:55: runtime error: signed integer overflow: 65535 * 65535
cannot be represented in type 'int'
当然,避免这种情况的方法是在进行乘法运算之前至少将类型转换为unsigned int
。只有一种情况会出现问题,那就是无符号类型的位数恰好等于int
的位数的一半。任何更小的情况都不会导致乘法溢出,例如std::uint8_t
;任何更大的情况都会导致类型恰好映射到升级等级之一,例如std::uint64_t
会匹配平台上的unsigned long
或unsigned long long
。但这真的很糟糕:它需要根据当前平台上
int
的大小来确定哪种类型会出现问题。是否有更好的方法可以避免使用#if
迷宫而避免使用无符号整数乘法时的未定义行为?
x *= x;
将uint16_t
提升为int32_t
?我在标准中找到了一些提升规则,但无法将它们精确地映射到这个问题上。 - towiuint8_t(a) * uint8_t(b)
并没有对无符号类型进行算术运算,因此不适用于控制无符号算术的子句。虽然意外,但是这是真的。 - Ben Voigt