可靠地检测整数上溢/下溢


Reliably detect integer overflow/underflow

我正在编写必须对计算结果执行以下操作的代码:

如果结果超过了可以用PHP的整数类型表示的限制,则抛出异常。

如果结果没有超过该限制,但确实导致生成浮点值,则发出警告并将结果四舍五入为整数。

我已经实现了以下方法:

const MAX = PHP_INT_MAX;
const MIN = (PHP_INT_MAX * -1) -1;
private function validateResult ($result)
{
    // Check that we still have an integer
    if (!is_int ($result))
    {
        // If the result is out of bounds for an integer then throw an exception
        if (($result > static::MAX) || ($result < static::MIN ))
        {
            // We've gone out of bounds
            throw new exception'AmountRangeException ("New value exceeds the limits of integer storage");
        }
        // If the result can be rounded into an integer then do so and issue
        // a warning.  
        trigger_error ("A non-integer value of $result resulted and has been rounded", E_USER_NOTICE);
        $result = (int) round ($result);
    }
    return $result;
}

但是,当尝试将1添加到PHP_INT_MAX时,它无法通过单元测试。我在PHP交互模式下尝试了以下操作:

php > var_dump (PHP_INT_MAX);
int(9223372036854775807)
php > var_dump (PHP_INT_MAX + 1);
double(9.2233720368548E+18)
php > var_dump ((PHP_INT_MAX + 1) > PHP_INT_MAX);
bool(false)
php > var_dump ((PHP_INT_MAX + 10) > PHP_INT_MAX);
bool(false)
php > var_dump ((PHP_INT_MAX + 100) > PHP_INT_MAX);
bool(false)
php > var_dump ((PHP_INT_MAX + 1000) > PHP_INT_MAX);
bool(false)
php > var_dump ((PHP_INT_MAX + 10000) > PHP_INT_MAX);
bool(true)

因此,看起来我的检测代码只有在结果超出范围约5个数量级的情况下才能工作。

由于我希望生成浮点值的和可以传递,只要结果可以四舍五入为整数,如果结果不是整数,那么简单地抛出异常是不符合要求的。

有没有一种可靠的方法来检测一个数字是否超出了整数范围,即使是很小的一部分?

更新:进一步的调查表明,在实际被认为大于PHP_INT_MAX之前,该值可能会超过1025。

php > var_dump ((PHP_INT_MAX + 1025) > PHP_INT_MAX);
bool(false)
php > var_dump ((PHP_INT_MAX + 1026) > PHP_INT_MAX);
bool(true)

更新2:我已经实现了一个临时修复程序,但这个修复程序非常粗糙和不雅,所以我不提这个问题,希望有人能有更好的建议。

if ((($result > static::MAX) || (($result == static::MAX) && ((string) $result != (string) static::MAX))) 
|| (($result < static::MIN) || (($result == static::MIN) && ((string) $result != (string) static::MIN)))) {}

这个想法是,如果根据PHP比较,数字在数学上是相同的,但在将数字转换为字符串后它们不相同,那么它们一定溢出了,但溢出量小于可以用>或<比较这似乎在单元测试中有效,但我真的不认为这是最好的解决方案,目前正在构建一组更严格的单元测试,看看值在边界以下、边界以上或边界上会发生什么

UPDATE 3:以上方法不适用于负溢出。如果结果触发负溢出,则结果为双精度,但其值仍与(PHP_INT_MAX*1)-1 相同

php > var_dump ((PHP_INT_MAX * -1) - 1);
int(-9223372036854775808)
php > var_dump ((PHP_INT_MAX * -1) - 2);
double(-9223372036854775808)

我一想到答案就非常简单。只需将MIN和MAX常数重新定义为最大的正整数值和负整数值,而是将它们定义为最大值,即当测试值和MIN/MAX值都转换为浮点值时,测试值仍将在MIN/MAX的范围内。

实验已经表明,使极限512小于绝对极限实现了这一点。

const MAX   = PHP_INT_MAX - 512;
const MIN   = (PHP_INT_MAX * -1) + 512;

现在,无论是否发生向浮点转换,都可以检测到该范围之外的任何值。

这种方法仍然存在一些问题(在32位系统上,退避区可能不需要这么大),但这是一种比类型杂耍和字符串比较更优雅的解决方案。