ChatGPT解决这个技术问题 Extra ChatGPT

如何检测无符号整数溢出?

我正在用 C++ 编写一个程序来查找 ab = c 的所有解决方案,其中 a、b 和 c 一起使用所有数字 0-9 恰好一次。该程序循环遍历 a 和 b 的值,并且每次在 a、b 和 ab 上运行一个数字计数例程,以检查数字条件是否满足。

但是,当 ab 超出整数限制时,可能会生成虚假解。我最终使用以下代码检查了这一点:

unsigned long b, c, c_test;
...
c_test=c*b;         // Possible overflow
if (c_test/b != c) {/* There has been an overflow*/}
else c=c_test;      // No overflow

有没有更好的方法来测试溢出?我知道有些芯片有一个在发生溢出时设置的内部标志,但我从未见过通过 C 或 C++ 访问它。

请注意,signed int 溢出是 C 和 C++ 中未定义的行为,因此您必须检测它而不实际导致它。对于加法前的有符号整数溢出,请参阅Detecting signed overflow in C/C++

Seacord 的 Secure Coding 是一个很好的资源,但不要使用 IntegerLib。请参阅blog.regehr.org/archives/593
gcc 编译器选项 -ftrapv 将导致它在(有符号)整数溢出时生成 SIGABRT。请参阅here
它没有回答溢出问题,但解决问题的另一种方法是使用像 GMP 这样的 BigNum 库来保证您始终具有足够的精度。如果您预先分配足够的数字,您将不必担心溢出。
@HeadGeek 在他的回答中给出的信息也正是我想说的。但是,还有一个补充。您现在检测乘法溢出的方式可能是最快的。正如我在 HeadGeek 的回答中所评论的那样,在 ARM 上,您可以使用 clz 指令或 __clz(unsigned) 函数来确定数字的等级(其最高位在哪里)。由于我不确定这是否在 x86 或 x64 上可用,我会假设它不是,并说找到最高有效位将需要最坏的 log(sizeof(int)*8) 指令。

M
Mirko Wf

我看到您使用的是无符号整数。根据定义,在 C 中(我不了解 C++),无符号算术不会溢出......所以,至少对于 C,你的观点没有实际意义:)

对于有符号整数,一旦发生溢出,就会发生 undefined behaviour (UB),并且您的程序可以执行任何操作(例如:渲染测试不确定)。 

#include <limits.h>

int a = <something>;
int x = <something>;
a += x;              /* UB */
if (a < 0) {         /* Unreliable test */
  /* ... */
}

要创建符合标准的程序,您需要在生成所述溢出之前测试溢出。该方法也可以用于无符号整数:

// For addition
#include <limits.h>

int a = <something>;
int x = <something>;
if (x > 0 && a > INT_MAX - x) // `a + x` would overflow
if (x < 0 && a < INT_MIN - x) // `a + x` would underflow

// For subtraction
#include <limits.h>
int a = <something>;
int x = <something>;
if (x < 0 && a > INT_MAX + x) // `a - x` would overflow
if (x > 0 && a < INT_MIN + x) // `a - x` would underflow

// For multiplication
#include <limits.h>

int a = <something>;
int x = <something>;
// There may be a need to check for -1 for two's complement machines.
// If one number is -1 and another is INT_MIN, multiplying them we get abs(INT_MIN) which is 1 higher than INT_MAX
if (a == -1 && x == INT_MIN) // `a * x` can overflow
if (x == -1 && a == INT_MIN) // `a * x` (or `a / x`) can overflow
// general case
if (x != 0 && a > INT_MAX / x) // `a * x` would overflow
if (x != 0 && a < INT_MIN / x) // `a * x` would underflow

对于除法(除了 INT_MIN-1 的特殊情况),不可能超过 INT_MININT_MAX


无符号整数在 C++ 中也不会严格溢出 (ISO/IEC 14882:2003 3.9.1.4)。我在问题中使用“溢出”是更通俗的意思,旨在包括明确定义的无符号类型包装,因为我对表示数学正整数的无符号整数感兴趣,而不是正整数 mod 2^32(或 2^ 64)。作为与数学无限大小整数行为的偏差的溢出与作为语言中未定义行为的溢出之间的区别似乎很少明确。
该测试不需要是 x >= 0 - x > 0 就足够了(如果 x == 0,那么 x + a 由于明显的原因不能溢出)。
@pmg,该标准是否有支持报价?
我喜欢这种方法...但是,请注意:乘法溢出检测假定为正 x。对于 x == 0,它会导致除以零检测,而对于负 x,它总是错误地检测到溢出。
if ((a < INT_MIN / x)) 测试为时已晚。首先需要进行 if (x == -1) 测试。
z
zneak

从 C23 开始,标准头文件 <stdckdint.h> 提供以下三个类似函数的宏:

bool ckd_add(type1 *result, type2 a, type3 b);
bool ckd_sub(type1 *result, type2 a, type3 b);
bool ckd_mul(type1 *result, type2 a, type3 b);

其中 type1type2type3 是任何整数类型。这些函数分别以任意精度加、减或乘 a 和 b,并将结果存储在 *result 中。如果结果不能用 type1 精确表示,则函数返回 true(“计算已溢出”)。 (任意精度是一种错觉;计算速度非常快,而且自 1990 年代初以来几乎所有可用的硬件都只需一两条指令即可完成。)

重写OP的例子:

unsigned long b, c, c_test;
// ...
if (ckd_mul(&c_test, c, b))
{
    // returned non-zero: there has been an overflow
}
else
{
    c = c_test; // returned 0: no overflow
}

c_test 包含所有情况下可能溢出的乘法结果。

早在 C23 之前,GCC 5+ 和 Clang 3.8+ 就提供了以相同方式工作的内置函数,只是结果指针最后而不是第一个传递:__builtin_add_overflow__builtin_sub_overflow__builtin_mul_overflow。这些也适用于小于 int 的类型。

unsigned long b, c, c_test;
// ...
if (__builtin_mul_overflow(c, b, &c_test))
{
    // returned non-zero: there has been an overflow
}
else
{
    c = c_test; // returned 0: no overflow
}

Clang 3.4+ 引入了具有固定类型的算术溢出内置函数,但它们的灵活性要差得多,而且 Clang 3.8 已经存在很长时间了。尽管有更方便的更新替代方案,但如果您需要使用它,请查找 __builtin_umull_overflow

Visual Studio 的 cl.exe 没有直接等效项。对于无符号加法和减法,包括 <intrin.h> 将允许您使用 addcarry_uNNsubborrow_uNN(其中 NN 是位数,如 addcarry_u8subborrow_u64)。他们的签名有点迟钝:

unsigned char _addcarry_u32(unsigned char c_in, unsigned int src1, unsigned int src2, unsigned int *sum);
unsigned char _subborrow_u32(unsigned char b_in, unsigned int src1, unsigned int src2, unsigned int *diff);

c_in/b_in 是输入的进位/借位标志,返回值是输出的进位/借位。它似乎没有符号运算或乘法的等价物。

否则,Windows 的 Clang 现在可以投入生产了(对于 Chrome 来说已经足够好了),所以这也是一种选择。


__builtin_sub_overflow 绝对不在 Clang 3.4 中。
@RichardCook,花了一些时间,但 Clang 具有 3.9 版的通用内置函数。
@tambre,我认为没有。
根据 docs__builtin_add_overflow 和朋友应该已经在 Clang 3.8 上可用。
谢谢。这很好用。知道 Visual C++ 的相应功能是什么吗?似乎找不到他们。
H
Head Geek

有一种方法可以确定操作是否可能溢出,使用操作数中最高有效位的位置和一些基本的二进制数学知识。

此外,任何两个操作数将导致(最多)比最大操作数的最高一位多一位。例如:

bool addition_is_safe(uint32_t a, uint32_t b) {
    size_t a_bits=highestOneBitPosition(a), b_bits=highestOneBitPosition(b);
    return (a_bits<32 && b_bits<32);
}

对于乘法,任何两个操作数将导致(最多)操作数位的总和。例如:

bool multiplication_is_safe(uint32_t a, uint32_t b) {
    size_t a_bits=highestOneBitPosition(a), b_bits=highestOneBitPosition(b);
    return (a_bits+b_bits<=32);
}

同样,您可以像这样估计 ab 次方结果的最大大小:

bool exponentiation_is_safe(uint32_t a, uint32_t b) {
    size_t a_bits=highestOneBitPosition(a);
    return (a_bits*b<=32);
}

(当然,用位数替换您的目标整数。)

我不确定确定数字中最高一位位置的最快方法,这是一种蛮力方法:

size_t highestOneBitPosition(uint32_t a) {
    size_t bits=0;
    while (a!=0) {
        ++bits;
        a>>=1;
    };
    return bits;
}

这并不完美,但这会让您很好地了解在您执行操作之前是否有任何两个数字可能溢出。由于 highestOneBitPosition 函数中的循环,我不知道它是否比简单地按照您建议的方式检查结果更快,但它可能会(特别是如果您事先知道操作数中有多少位)。


当然,您可以将highestOneBitPosition 重命名为log :)
是的,它与 log2 的操作相同,但对于没有数学背景的人来说,这并不一定那么明显。
这个算法不会低估安全答案吗? 2^31 + 0 将检测为不安全,因为最高OneBitPosition(2^31) = 32。(2^32 - 1) * 1 将检测为不安全,因为 32 + 1 > 32。1 ^ 100 将检测为不安全,因为 1 * 100 > 32。
根据您的 multiplication_is_safe 0x8000 * 0x10000 会溢出(位位置是 16 + 17 = 33,即 > 32),尽管它不是因为 0x8000 * 0x10000 = 0x80000000 显然仍然适合无符号32 位整数。这只是此代码不起作用的示例之一。 0x8000 * 0x10001,...
这几乎没用。当它返回“安全”时 - 它是。否则,仍然需要执行完全乘法以确保它确实是安全的。鉴于报告假阴性的值范围可能很大,当存在返回正确答案的算法而没有验证步骤时,这没有真正的价值。
R
Robert Gamble

一些编译器提供对 CPU 中整数溢出标志的访问,然后您可以对其进行测试,但这不是标准的。

您还可以在执行乘法之前测试溢出的可能性:

if ( b > ULONG_MAX / a ) // a * b would overflow

...或使用 numeric_limits::max()
不要忘记处理 a=0 ——然后除法中断。
@Thelema:“不要忘记处理 a=0” - 和 INT_MIN / -1。
如果 b == ULONG_MAX / a 怎么办?那么它仍然可以拟合,因为 a 除以 ULONG_MAX 而没有残差。
有趣的是,在性能方面,与除法相比,乘法相当快,并且您为每个乘法添加一个除法。这听起来不像是解决方案。
P
Peter Mortensen

警告:使用 -O2 编译时,GCC 可以优化掉溢出检查。在某些情况下,选项 -Wall 会给您一个警告,例如

if (a + b < a) { /* Deal with overflow */ }

但不是在这个例子中:

b = abs(a);
if (b < 0) { /* Deal with overflow */ }

唯一安全的方法是在溢出发生之前检查溢出,如 CERT paper 中所述,系统地使用这将非常乏味。

使用 -fwrapv 进行编译可以解决问题,但会禁用一些优化。

我们迫切需要一个更好的解决方案。我认为编译器在进行不依赖于溢出的优化时应该默认发出警告。目前的情况允许编译器优化溢出检查,这在我看来是不可接受的。


请注意,编译器只能对有符号整数类型执行此操作;溢出是为无符号整数类型完全定义的。不过,是的,这是一个相当危险的陷阱!
“我认为编译器在进行不依赖溢出的优化时应该默认发出警告。” - 那么for(int k = 0; k < 5; k++) {...} 应该发出警告吗?
@immibis:为什么要这样做? k 的值可以在编译时轻松确定。编译器不必做任何假设。
@immibis:引用上面的话:“我认为编译器在进行依赖于溢出的优化时默认情况下应该发出警告。”
@MikeMB 在发出仅使用 n 低 5 位的移位指令之前,编译器无需检查 n 是否小于 32 的优化?
P
Peter Mortensen

Clang 现在支持有符号和无符号整数的动态溢出检查。请参阅 -fsanitize=integer 开关。目前,它是唯一一个完全支持用于调试目的的动态溢出检查的 C++ 编译器。


P
Peter Mortensen

我看到很多人回答了关于溢出的问题,但我想解决他原来的问题。他说问题是要找到 ab=c 以便所有数字都被使用而不重复。好吧,这不是他在这篇文章中问的,但我仍然认为有必要研究问题的上限并得出结论,他永远不需要计算或检测溢出(注意:我不精通在数学中,所以我一步一步地做了这个,但最终的结果是如此简单,以至于这可能有一个简单的公式)。

要点是问题要求 a、b 或 c 的上限是 98.765.432。无论如何,首先将问题分为琐碎和非琐碎部分:

x0 == 1(9、8、7、6、5、4、3、2的所有排列都是解)

x1 == x(没有解决办法)

0b == 0(无解)

1b == 1(无解)

ab, a > 1, b > 1(非平凡)

现在我们只需要证明没有其他解决方案是可能的,只有排列是有效的(然后打印它们的代码很简单)。我们回到上限。实际上上限是 c ≤ 98.765.432。它是上限,因为它是 8 位数字中的最大数字(a 和 b 共 10 位数字减 1)。这个上限仅适用于 c,因为 a 和 b 的边界必须低得多,因为我们可以计算出指数增长,将 b 从 2 变为上限:

    9938.08^2 == 98765432
    462.241^3 == 98765432
    99.6899^4 == 98765432
    39.7119^5 == 98765432
    21.4998^6 == 98765432
    13.8703^7 == 98765432
    9.98448^8 == 98765432
    7.73196^9 == 98765432
    6.30174^10 == 98765432
    5.33068^11 == 98765432
    4.63679^12 == 98765432
    4.12069^13 == 98765432
    3.72429^14 == 98765432
    3.41172^15 == 98765432
    3.15982^16 == 98765432
    2.95305^17 == 98765432
    2.78064^18 == 98765432
    2.63493^19 == 98765432
    2.51033^20 == 98765432
    2.40268^21 == 98765432
    2.30883^22 == 98765432
    2.22634^23 == 98765432
    2.15332^24 == 98765432
    2.08826^25 == 98765432
    2.02995^26 == 98765432
    1.97741^27 == 98765432

请注意,例如最后一行:它表示 1.97^27 ~98M。因此,例如, 1^27 == 1 和 2^27 == 134.217.728 这不是一个解决方案,因为它有 9 位数字(2 > 1.97 所以它实际上比应该测试的要大)。可以看出,可用于测试 a 和 b 的组合非常小。对于 b == 14,我们需要尝试 2 和 3。对于 b == 3,我们从 2 开始,在 462 停止。所有结果都被授予小于 ~98M。

现在只需测试上面的所有组合并寻找不重复任何数字的组合:

    ['0', '2', '4', '5', '6', '7', '8'] 84^2 = 7056
    ['1', '2', '3', '4', '5', '8', '9'] 59^2 = 3481
    ['0', '1', '2', '3', '4', '5', '8', '9'] 59^2 = 3481 (+leading zero)
    ['1', '2', '3', '5', '8'] 8^3 = 512
    ['0', '1', '2', '3', '5', '8'] 8^3 = 512 (+leading zero)
    ['1', '2', '4', '6'] 4^2 = 16
    ['0', '1', '2', '4', '6'] 4^2 = 16 (+leading zero)
    ['1', '2', '4', '6'] 2^4 = 16
    ['0', '1', '2', '4', '6'] 2^4 = 16 (+leading zero)
    ['1', '2', '8', '9'] 9^2 = 81
    ['0', '1', '2', '8', '9'] 9^2 = 81 (+leading zero)
    ['1', '3', '4', '8'] 3^4 = 81
    ['0', '1', '3', '4', '8'] 3^4 = 81 (+leading zero)
    ['2', '3', '6', '7', '9'] 3^6 = 729
    ['0', '2', '3', '6', '7', '9'] 3^6 = 729 (+leading zero)
    ['2', '3', '8'] 2^3 = 8
    ['0', '2', '3', '8'] 2^3 = 8 (+leading zero)
    ['2', '3', '9'] 3^2 = 9
    ['0', '2', '3', '9'] 3^2 = 9 (+leading zero)
    ['2', '4', '6', '8'] 8^2 = 64
    ['0', '2', '4', '6', '8'] 8^2 = 64 (+leading zero)
    ['2', '4', '7', '9'] 7^2 = 49
    ['0', '2', '4', '7', '9'] 7^2 = 49 (+leading zero)

它们都不符合问题(也可以通过缺少'0','1',...,'9'来看出)。

解决它的示例代码如下。另请注意,它是用 Python 编写的,不是因为它需要任意精度的整数(代码不会计算大于 9800 万的任何东西),而是因为我们发现测试的数量太少了,我们应该使用高级语言来利用其内置的容器和库(另请注意:代码有 28 行)。

    import math

    m = 98765432
    l = []
    for i in xrange(2, 98765432):
        inv = 1.0/i
        r = m**inv
        if (r < 2.0): break
        top = int(math.floor(r))
        assert(top <= m)

        for j in xrange(2, top+1):
            s = str(i) + str(j) + str(j**i)
            l.append((sorted(s), i, j, j**i))
            assert(j**i <= m)

    l.sort()
    for s, i, j, ji in l:
        assert(ji <= m)
        ss = sorted(set(s))
        if s == ss:
            print '%s %d^%d = %d' % (s, i, j, ji)

        # Try with non significant zero somewhere
        s = ['0'] + s
        ss = sorted(set(s))
        if s == ss:
            print '%s %d^%d = %d (+leading zero)' % (s, i, j, ji)

为什么不使用 9.876.543.210 作为上限?
因为等式的左边必须使用 2 位数字。
并不是说它有什么不同,但实际上可以将上限视为 98765410,因为您已经说过 LHS 上的值 > 1
P
Peter Mortensen

这是该问题的“非便携式”解决方案。 Intel x86 和 x64 CPU 有所谓的 EFLAGS-register,它在每次整数算术运算后由处理器填充。我将在这里跳过详细描述。相关标志是“溢出”标志(掩码 0x800)和“进位”标志(掩码 0x1)。为了正确解释它们,应该考虑操作数是有符号还是无符号类型。

这是从 C/C++ 检查标志的实用方法。以下代码适用于 Visual Studio 2005 或更高版本(32 位和 64 位)以及 GNU C/C++ 64 位。

#include <cstddef>
#if defined( _MSC_VER )
#include <intrin.h>
#endif

inline size_t query_intel_x86_eflags(const size_t query_bit_mask)
{
    #if defined( _MSC_VER )

        return __readeflags() & query_bit_mask;

    #elif defined( __GNUC__ )
        // This code will work only on 64-bit GNU-C machines.
        // Tested and does NOT work with Intel C++ 10.1!
        size_t eflags;
        __asm__ __volatile__(
            "pushfq \n\t"
            "pop %%rax\n\t"
            "movq %%rax, %0\n\t"
            :"=r"(eflags)
            :
            :"%rax"
            );
        return eflags & query_bit_mask;

    #else

        #pragma message("No inline assembly will work with this compiler!")
            return 0;
    #endif
}

int main(int argc, char **argv)
{
    int x = 1000000000;
    int y = 20000;
    int z = x * y;
    int f = query_intel_x86_eflags(0x801);
    printf("%X\n", f);
}

如果操作数相乘而没有溢出,您会从 query_intel_eflags(0x801) 得到返回值 0,即既没有设置进位也没有设置溢出标志。在提供的 main() 示例代码中,发生了溢出并且两个标志都设置为 1。这个检查并不意味着任何进一步的计算,所以它应该很快。


这不会调用未定义的行为吗?有符号溢出是未定义的行为。如果我错了,请纠正我,但即使你不使用结果,你也会得到 UB。 stackoverflow.com/questions/16188263/…
如果您想避免 UB,您可能也必须在汇编中进行乘法运算。
D
DX-MON

这是一种非常快速的方法来检测至少加法的溢出,这可能会导致乘法、除法和幂运算。

这个想法正是因为处理器只会让值回零,并且 C/C++ 是从任何特定处理器抽象出来的,所以您可以:

uint32_t x, y;
uint32_t value = x + y;
bool overflow = value < (x | y);

这既确保了如果一个操作数为零而一个不是,则不会错误地检测到溢出,并且比之前建议的许多 NOT/XOR/AND/test 操作要快得多。

正如所指出的,这种方法虽然比其他更复杂的方法更好,但仍然是可优化的。以下是包含优化的原始代码的修订版:

uint32_t x, y;
uint32_t value = x + y;
const bool overflow = value < x; // Alternatively "value < y" should also work

检测乘法溢出的一种更有效、更便宜的方法是:

uint32_t x, y;
const uint32_t a = (x >> 16U) * (y & 0xFFFFU);
const uint32_t b = (x & 0xFFFFU) * (y >> 16U);
const bool overflow = ((x >> 16U) * (y >> 16U)) +
    (a >> 16U) + (b >> 16U);
uint32_t value = overflow ? UINT32_MAX : x * y;

这会导致 UINT32_MAX 溢出或乘法结果。在这种情况下,允许对有符号整数进行乘法运算是严格未定义的行为。

值得注意的是,这使用部分 Karatsuba 方法乘法分解来计算 64 位乘法的高 32 位,以检查是否应设置其中任何一个以了解 32 位乘法是否溢出。

如果使用 C++,你可以把它变成一个简洁的小 lambda 来计算溢出,这样检测器的内部工作就被隐藏了:

uint32_t x, y;
const bool overflow
{
    [](const uint32_t x, const uint32_t y) noexcept -> bool
    {
        const uint32_t a{(x >> 16U) * uint16_t(y)};
        const uint32_t b{uint16_t(x) * (y >> 16U)};
        return ((x >> 16U) * (y >> 16U)) + (a >> 16U) + (b >> 16U);
    }(x, y)
};
uint32_t value{overflow ? UINT32_MAX : x * y};

由于计算理论,我不同意..考虑以下问题:y > x,值溢出,y 仅大于 x,因为设置了符号位(1 + 255,例如,对于无符号字符)测试值和 x 会导致在溢出 = false - 因此使用逻辑或防止这种破坏行为..
该测试适用于您给出的数字 (x:=1, y:=255, size = uint8_t):值将为 0 (1+255) 并且 0<1 为真。它确实适用于每个数字对。
如果有溢出,则比 x+y>=256value=x+y-256。因为 y<256 始终为真,所以 (y-256) 为负,因此 value < x 始终为真。非溢出情况的证明非常相似。
@DX-MON:如果您还有来自先前添加的进位位,则您的第一种方法是必要的。 uint32_t x[N], y[N], z[N], carry=0; for (int i = 0; i < N; i++) { z[i] = x[i] + y[i] + carry; carry = z[i] < (x[i] | y[i]); } 如果您不or 值,您将无法区分一个操作数和进位位为零以及一个操作数是 0xffffffff 和进位位为 1。
@Matt,当 x[i]y[i] 均为 0xFFFFFFFF 且 carry 为 1 时失败。您必须在添加进位之前测试溢出,此时您不妨放弃 |
P
Peter Mortensen

如果您的数据类型大于您要测试的数据类型(假设您执行 32 位加法并且您有 64 位类型),那么这将检测是否发生溢出。我的示例是 8 位加法。但它可以按比例放大。

uint8_t x, y;    /* Give these values */
const uint16_t data16    = x + y;
const bool carry        = (data16 > 0xFF);
const bool overflow     = ((~(x ^ y)) & (x ^ data16) & 0x80);

它基于此页面上解释的概念:http://www.cs.umd.edu/class/spring2003/cmsc311/Notes/Comb/overflow.html

对于 32 位示例,0xFF 变为 0xFFFFFFFF0x80 变为 0x80000000,最后 uint16_t 变为 uint64_t

注意:这会捕获整数加法/减法溢出,我意识到您的问题涉及乘法。在这种情况下,分工可能是最好的方法。这通常是 calloc 实现确保参数不会溢出的一种方式,因为它们被相乘以获得最终大小。


链接已损坏:HTTP 403:禁止
A
Andrew Edgecombe

最简单的方法是将 unsigned long 转换为 unsigned long long,进行乘法运算,然后将结果与 0x100000000LL 进行比较。

您可能会发现这比您在示例中所做的除法更有效。

哦,它可以在 C 和 C++ 中工作(因为你已经用两者标记了这个问题)。

刚刚看了一下glibc manual。在 SIGFPE 中提到了整数溢出陷阱 (FPE_INTOVF_TRAP)。这将是理想的,除了手册中令人讨厌的部分:

FPE_INTOVF_TRAP 整数溢出(在 C 程序中不可能,除非您以特定于硬件的方式启用溢出捕获)。

真的有点可惜。


嘿...我没有说的是,我问这个问题是为了准备编写一个程序来解决更大数字的问题,其中我已经在使用 long long int。由于 long long int 不是(据称)在 C++ 标准中,我坚持使用 32 位版本以避免混淆。
我建议使用 ULONG_MAX,它比硬编码 0x100000000 更易于键入且更便携。
这在 longlong long 大小相同时不起作用(例如在许多 64 位编译器上)。
无论如何,依靠信号告诉你溢出会非常慢。
@SamB 仅当预期会频繁溢出时。
P
Peter Mortensen

您无法从 C/C++ 访问溢出标志。

一些编译器允许您在代码中插入陷阱指令。在 GCC 上,选项是 -ftrapv

您可以做的唯一可移植且独立于编译器的事情是自己检查溢出。就像您在示例中所做的那样。

但是,-ftrapv 在使用最新 GCC 的 x86 上似乎没有任何作用。我想这是旧版本的遗留物或特定于其他架构的。我曾期望编译器在每次添加后插入一个 INTO 操作码。不幸的是,它没有这样做。


也许它会有所不同: -ftrapv 在 Cygwin 机器上使用 GCC 4.3.4 似乎可以正常工作。 stackoverflow.com/questions/5005379/… 有一个示例
你们俩都是对的。 -ftrapv 完成这项工作,但仅适用于有符号整数
P
Peter Mortensen

对于无符号整数,只需检查结果是否小于参数之一:

unsigned int r, a, b;
r = a + b;
if (r < a)
{
    // Overflow
}

对于有符号整数,您可以检查参数和结果的符号。

不同符号的整数不能溢出,只有当结果是不同符号时,相同符号的整数才会溢出:

signed int r, a, b, s;
r = a + b;
s = a>=0;
if (s == (b>=0) && s != (r>=0))
{
    // Overflow
}

那么第一种方法也适用于有符号整数,不是吗? char result = (char)127 + (char)3; 为 -126;小于两个操作数。
哦,我明白了,问题在于它对于有符号类型是未定义的。
-1 有符号数溢出会导致未定义的行为(因此测试为时已晚,无法真正有用)。
@primfaktor 它不适用于有符号整数:char((-127) + (-17)) = 112。对于有符号整数,您必须检查参数和结果的符号位
如前所述,有符号整数的解决方案不起作用,因为在溢出的情况下 a + b 的未定义行为。必须在操作之前检查带符号整数的溢出。
P
Paul Chernoch

我需要为浮点数回答同样的问题,其中位掩码和移位看起来没有希望。我选择的方法适用于有符号和无符号、整数和浮点数。即使没有更大的数据类型可用于中间计算,它也可以工作。对于所有这些类型,它并不是最有效的,但因为它对所有类型都有效,所以值得使用。

有符号溢出测试,加法和减法:

获取表示类型 MAXVALUE 和 MINVALUE 的最大和最小可能值的常量。计算并比较操作数的符号。一个。如果任一值为零,则加法和减法都不会溢出。跳过剩余的测试。湾。如果符号相反,则加法不会溢出。跳过剩余的测试。 C。如果符号相同,则减法不会溢出。跳过剩余的测试。测试 MAXVALUE 的正溢出。一个。如果两个符号均为正且 MAXVALUE - A < B,则加法将溢出。湾。如果 B 的符号为负且 MAXVALUE - A < -B,则减法将溢出。测试 MINVALUE 的负溢出。一个。如果两个符号都为负且 MINVALUE - A > B,则加法将溢出。湾。如果 A 的符号为负且 MINVALUE - A > B,则减法将溢出。否则,不会溢出。

有符号溢出测试,乘法和除法:

获取表示类型 MAXVALUE 和 MINVALUE 的最大和最小可能值的常量。计算操作数的大小(绝对值)并将其与 1 进行比较。 (下面,假设 A 和 B 是这些量级,而不是签名的原件。)如果任一值为零,则乘法不会溢出,除法将产生零或无穷大。湾。如果任一值为 1,则乘法和除法不会溢出。 C。如果一个操作数的大小小于 1,而另一个操作数大于 1,则乘法不会溢出。 d。如果幅度都小于 1,则除法不会溢出。测试 MAXVALUE 的正溢出。一个。如果两个操作数都大于 1 并且 MAXVALUE / A < B,则乘法将溢出。湾。如果 B 小于 1 并且 MAXVALUE * B < A,则除法将溢出。否则,不会溢出。

注意:MINVALUE 的最小溢出由 3 处理,因为我们取的是绝对值。但是,如果 ABS(MINVALUE) > MAXVALUE,那么我们会遇到一些罕见的误报。

下溢测试类似,但涉及 EPSILON(大于零的最小正数)。


至少在 POSIX 系统上,可以为浮点下溢/溢出启用 SIGFPE 信号。
在转换为浮点和返回工作时,它(根据我在 32 位机器上的测试)比其他解决方案慢得多。
审阅者检测到减法第 2 部分的缺失案例。我同意 0 - MINVALUE 会溢出。所以应该增加对这种情况的测试。
<pedantic>整数不会下溢(= 变得太接近零而无法以任何精度表示)。 1.0e-200 / 1.0e200 将是实际下溢的示例,假设 IEEE 双打。相反,这里的正确术语是负溢出。</pedantic>
准确地说,整数不被视为下溢的原因是由于定义了截断行为,例如 1/INT_MAX 很可能被视为下溢,但该语言只是要求截断为零。
R
Robert C. Seacord

CERT 开发了一种使用“as-if”无限范围 (AIR) 整数模型检测和报告有符号整数溢出、无符号整数换行和整数截断的新方法。 CERT 发布了一个描述该模型的 technical report,并制作了一个基于 GCC 4.4.0 和 GCC 4.5.0 的工作原型。

AIR 整数模型要么生成一个与使用无限范围整数获得的值相等的值,要么导致违反运行时约束。与以前的整数模型不同,AIR 整数不需要精确的陷阱,因此不会破坏或抑制大多数现有优化。


我在链接上没有看到任何有用的东西,但这听起来像是我长期以来提倡的一种模式。它支持绝大多数有用的优化,同时还支持大多数实现基本上免费提供的有用语义保证。如果代码知道函数的输入在输出重要的所有情况下都是有效的,但事先不知道输出是否重要,那么在它们不会影响任何事情的情况下让溢出发生可能是比不惜一切代价阻止它们更容易和更有效。
P
Peter Mortensen

另一个有趣的工具是 IOC: An Integer Overflow Checker for C/C++

这是一个修补的 Clang 编译器,它在编译时向代码添加检查。

您会得到如下所示的输出:

CLANG ARITHMETIC UNDEFINED at <add.c, (9:11)> :
Op: +, Reason : Signed Addition Overflow,
BINARY OPERATION: left (int32): 2147483647 right (int32): 1

此补丁现在已合并到其他消毒剂中的 clang 代码库,请参阅我的答案。
P
Peter Mortensen

使用汇编语言的解决方案的另一种变体是外部过程。此示例用于在 Linux x64 下使用 g++ 和 fasm 进行无符号整数乘法。

此过程将两个无符号整数参数(32 位)相乘(根据 amd64 的 specification3.2.3 参数传递节)。

如果类是 INTEGER,则使用序列 %rdi、%rsi、%rdx、%rcx、%r8 和 %r9 的下一个可用寄存器

(我的代码中的 edi 和 esi 寄存器))并返回结果,如果发生溢出则返回 0。

format ELF64

section '.text' executable

public u_mul

u_mul:
  MOV eax, edi
  mul esi
  jnc u_mul_ret
  xor eax, eax
u_mul_ret:
ret

测试:

extern "C" unsigned int u_mul(const unsigned int a, const unsigned int b);

int main() {
    printf("%u\n", u_mul(4000000000,2)); // 0
    printf("%u\n", u_mul(UINT_MAX/2,2)); // OK
    return 0;
}

将程序与 asm 目标文件链接。在我的例子中,在 Qt Creator 中,将其添加到 .pro 文件中的 LIBS


S
SamB

用双打计算结果。它们有 15 位有效数字。您的要求在 c 上有一个 108 的硬上限——它最多可以有 8 位数字。因此,如果它在范围内,结果将是精确的,否则它不会溢出。


S
Syon

试试这个宏测试32位机器的溢出位(改编了Angel Sinigersky的解决方案)

#define overflowflag(isOverflow){   \
size_t eflags;                      \
asm ("pushfl ;"                     \
     "pop %%eax"                    \
    : "=a" (eflags));               \
isOverflow = (eflags >> 11) & 1;}

我将其定义为宏,否则溢出位将被覆盖。

随后是一个带有上述代码段的小应用程序:

#include <cstddef>
#include <stdio.h>
#include <iostream>
#include <conio.h>
#if defined( _MSC_VER )
#include <intrin.h>
#include <oskit/x86>
#endif

using namespace std;

#define detectOverflow(isOverflow){     \
size_t eflags;                      \
asm ("pushfl ;"                     \
    "pop %%eax"                     \
    : "=a" (eflags));               \
isOverflow = (eflags >> 11) & 1;}

int main(int argc, char **argv) {

    bool endTest = false;
    bool isOverflow;

    do {
        cout << "Enter two intergers" << endl;
        int x = 0;
        int y = 0;
        cin.clear();
        cin >> x >> y;
        int z = x * y;
        detectOverflow(isOverflow)
        printf("\nThe result is: %d", z);
        if (!isOverflow) {
            std::cout << ": no overflow occured\n" << std::endl;
        } else {
            std::cout << ": overflow occured\n" << std::endl;
        }

        z = x * x * y;
        detectOverflow(isOverflow)
        printf("\nThe result is: %d", z);
        if (!isOverflow) {
            std::cout << ": no overflow ocurred\n" << std::endl;
        } else {
            std::cout << ": overflow occured\n" << std::endl;
        }

        cout << "Do you want to stop? (Enter \"y\" or \"Y)" << endl;

        char c = 0;

        do {
            c = getchar();
        } while ((c == '\n') && (c != EOF));

        if (c == 'y' || c == 'Y') {
            endTest = true;
        }

        do {
            c = getchar();
        } while ((c != '\n') && (c != EOF));

    } while (!endTest);
}

并非所有 32 位机器都与 Intel x86 兼容,并且并非所有编译器都支持 gnu 汇编语法(我发现您发布测试 _MSC_VER 的代码很有趣,尽管 MS 编译都会拒绝该代码)。
C
Community

Catching Integer Overflows in C 指出了一种比 CERT 讨论的解决方案更通用的解决方案(它在处理类型方面更通用),即使它需要一些 GCC 扩展(我不知道它们的支持范围有多广)。


P
Peter Mortensen

您无法从 C/C++ 访问溢出标志。

我不同意这一点。假设您在 x86 上,您可以编写一些内联汇编语言并使用 jo(跳转溢出)指令来捕获溢出。当然,您的代码将不再可移植到其他架构。

查看 info asinfo gcc


内联汇编器没有 C/C++ 特性和平台独立。在 x86 上,您可以使用 into 指令而不是分支 btw。
h
hsivonen

mozilla::CheckedInt<T> 为整数类型 T 提供溢出检查的整数数学运算(使用 clang 和 gcc 上的编译器内在函数)。该代码在 MPL 2.0 下,并且依赖于三个(IntegerTypeTraits.hAttributes.hCompiler.h)其他仅标头的非标准库标头以及 Mozilla 特定的 assertion machinery。如果您导入代码,您可能想要替换断言机制。


S
Steztric

为了扩展 Head Geek 的答案,有一种更快的方法来执行 addition_is_safe

bool addition_is_safe(unsigned int a, unsigned int b)
{
    unsigned int L_Mask = std::numeric_limits<unsigned int>::max();
    L_Mask >>= 1;
    L_Mask = ~L_Mask;

    a &= L_Mask;
    b &= L_Mask;

    return ( a == 0 || b == 0 );
}

这使用机器架构安全,因为 64 位和 32 位无符号整数仍然可以正常工作。基本上,我创建了一个掩码,它将屏蔽除最重要的位之外的所有内容。然后,我屏蔽两个整数,如果其中任何一个都没有设置该位,那么加法是安全的。

如果您在某些构造函数中预先初始化掩码,这会更快,因为它永远不会改变。


这是不正确的。进位可能会带来来自较低位置的位,这将导致溢出。考虑添加 UINT_MAX + 1。屏蔽后,a 将设置高位,但 1 将变为零,因此该函数将返回 true,添加是安全的 - 但您会直接走向溢出。
P
Pauli Nieminen

x86 指令集包括一个无符号乘法指令,将结果存储到两个寄存器中。要使用 C 中的该指令,可以在 64 位程序 (GCC) 中编写以下代码:

unsigned long checked_imul(unsigned long a, unsigned long b) {
  unsigned __int128 res = (unsigned __int128)a * b;
  if ((unsigned long)(res >> 64))
    printf("overflow in integer multiply");
  return (unsigned long)res;
}

对于 32 位程序,需要将结果设为 64 位,将参数设为 32 位。

另一种方法是使用编译器相关的内在函数来检查标志寄存器。可以从 6.56 Built-in Functions to Perform Arithmetic with Overflow Checking 找到溢出内在函数的 GCC 文档。


您应该使用无符号 128 位类型 __uint128 以避免有符号溢出和右移负值。
什么是“编译器依赖本能”“溢出本能”?你是说intrinsic functions吗?你有参考吗? (请通过 editing your answer 回复,而不是在评论中(视情况而定)。)
P
Peter Mortensen

一个干净的方法是覆盖所有运算符(特别是 + 和 *)并在执行操作之前检查溢出。


除了您不能覆盖内置类型的运算符。您需要为此编写一个类并重写客户端代码以使用它。
P
Peter Mortensen

MSalter's answer 是个好主意。

如果需要整数计算(为了精度),但浮点可用,您可以执行以下操作:

uint64_t foo(uint64_t a, uint64_t b) {
    double dc;

    dc = pow(a, b);

    if (dc < UINT_MAX) {
       return (powu64(a, b));
    }
    else {
      // Overflow
    }
}

通常,我会说以浮点数重复计算是一个坏主意,但对于这种特定情况的幂 a^c,它可能更有效。但测试应该是 (c * log(a) < max_log),其中 const double max_log = log(UINT_MAX)
P
Peter Mortensen

这取决于你用它做什么。执行无符号长 (DWORD) 加法或乘法,最好的解决方案是使用 ULARGE_INTEGER。

ULARGE_INTEGER 是两个 DWORD 的结构。完整值可以作为“QuadPart”访问,而高 DWORD 可以作为“HighPart”访问,低 DWORD 可以作为“LowPart”访问。

例如:

DWORD
My Addition(DWORD Value_A, DWORD Value_B)
{
    ULARGE_INTEGER a, b;

    b.LowPart = Value_A;  // A 32 bit value(up to 32 bit)
    b.HighPart = 0;
    a.LowPart = Value_B;  // A 32 bit value(up to 32 bit)
    a.HighPart = 0;

    a.QuadPart += b.QuadPart;

    // If  a.HighPart
    // Then a.HighPart contains the overflow (carry)

    return (a.LowPart + a.HighPart)

    // Any overflow is stored in a.HighPart (up to 32 bits)

不幸的是,这是一个仅限 Windows 的解决方案。其他平台没有 ULARGE_INTEGER
T
Tyler Durden

要以可移植的方式执行无符号乘法而不溢出,可以使用以下方法:

... /* begin multiplication */
unsigned multiplicand, multiplier, product, productHalf;
int zeroesMultiplicand, zeroesMultiplier;
zeroesMultiplicand = number_of_leading_zeroes( multiplicand );
zeroesMultiplier   = number_of_leading_zeroes( multiplier );
if( zeroesMultiplicand + zeroesMultiplier <= 30 ) goto overflow;
productHalf = multiplicand * ( c >> 1 );
if( (int)productHalf < 0 ) goto overflow;
product = productHalf * 2;
if( multiplier & 1 ){
   product += multiplicand;
   if( product < multiplicand ) goto overflow;
}
..../* continue code here where "product" is the correct product */
....
overflow: /* put overflow handling code here */

int number_of_leading_zeroes( unsigned value ){
   int ctZeroes;
   if( value == 0 ) return 32;
   ctZeroes = 1;
   if( ( value >> 16 ) == 0 ){ ctZeroes += 16; value = value << 16; }
   if( ( value >> 24 ) == 0 ){ ctZeroes +=  8; value = value <<  8; }
   if( ( value >> 28 ) == 0 ){ ctZeroes +=  4; value = value <<  4; }
   if( ( value >> 30 ) == 0 ){ ctZeroes +=  2; value = value <<  2; }
   ctZeroes -= x >> 31;
   return ctZeroes;
}

W
Walery Strauch
#include <stdio.h>
#include <stdlib.h>

#define MAX 100 

int mltovf(int a, int b)
{
    if (a && b) return abs(a) > MAX/abs(b);
    else return 0;
}

main()
{
    int a, b;

    for (a = 0; a <= MAX; a++)
        for (b = 0; b < MAX; b++) {

        if (mltovf(a, b) != (a*b > MAX)) 
            printf("Bad calculation: a: %d b: %d\n", a, b);

    }
}

T
Toby Speight

测试溢出的简单方法是通过检查当前值是否小于先前值来进行验证。例如,假设您有一个循环来打印 2 的幂:

long lng;
int n;
for (n = 0; n < 34; ++n)
{
   lng = pow (2, n);
   printf ("%li\n", lng);
}

以我描述的方式添加溢出检查会导致:

long signed lng, lng_prev = 0;
int n;
for (n = 0; n < 34; ++n)
{
    lng = pow (2, n);
    if (lng <= lng_prev)
    {
        printf ("Overflow: %i\n", n);
        /* Do whatever you do in the event of overflow.  */
    }
    printf ("%li\n", lng);
    lng_prev = lng;
}

它适用于无符号值以及正负符号值。

当然,如果您想对减少值而不是增加值执行类似的操作,您可以翻转 <= 符号使其变为 >=,假设下溢的行为与上溢的行为相同。老实说,这与您在不访问 CPU 的溢出标志的情况下所获得的可移植性差不多(这将需要内联汇编代码,从而使您的代码无论如何都不可移植)。


如果有符号值溢出,则程序的行为未定义。不保证环绕。