【发布时间】:2014-09-07 20:37:08
【问题描述】:
假设您正在使用<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 迷宫,是否有更好的方法可以避免无符号整数乘法的未定义行为?
【问题讨论】:
-
@TC:这听起来效率很低。 64 位乘法可能很慢。
-
后续问题:是什么规则/促销使
x *= x;将uint16_t提升为int32_t?我在 Std 中找到了一些提升规则,但无法准确地将它们映射到此。 -
@BenVoigt 标准中有一条错综复杂的路径,即由于有符号整数溢出,两个无符号短路的乘法导致无符号短路会为某些数据值产生未定义的行为。标准中有更直接的语句说,对相同的无符号数据类型产生相同类型结果的算术运算不可能溢出,结果就像模数运算并被使用,这是任何合理的程序员所期望的,包括我自己。标准自相矛盾。
-
@amdn:
uint8_t(a) * uint8_t(b)不对无符号类型进行算术运算,因此控制无符号算术的子句不适用。出乎意料,但确实如此。 -
当您需要复杂的解决方案(例如该问题的答案)以将句法糖添加到对标准的直接语义解释中时,丹麦的某些东西已经腐烂了。
标签: c++ portability undefined-behavior multiplication