C++浮点数转换为整数的溢出问题

C++语言提供了将浮点数转换为整数的功能。但是,如果要转换的浮点数超出了整数的表示范围,会得到怎样的结果呢?这种情况在标准中是未定义行为(隐式转换 – cppreference.com)。例如,在x64架构上运行以下测试代码:

std::cout << static_cast<int32_t>(std::pow(2, 31)) << std::endl;

运行结果为-2147483648,与我们的原始浮点数的数值差了十万八千里。为了避免这种现象发生,我们似乎可以在进行转换前先使用std::clamp函数限制浮点数的范围。然而这真的有用吗?让我们试试下面这段测试代码:

double source = std::pow(2, 100);
double clamped = std::clamp(source, std::numeric_limits<int64_t>::min(), std::numeric_limits<int64_t>::max());
int64_t converted = clamped;
std::cout << converted << std::endl;

不出意外的话,是要发生意外了。输出结果为-9223372036854775808。为什么会发生这种事情呢?在调试器中不难发现,变量clamped实际值为9223372036854775808,比int64_t类型可以表示的最大值9223372036854775807恰好大了1。这是因为double类型的精度并不足以准确存储这个最大值(通常来说如此,实际上C++语言标准并未规定浮点数类型的具体表示形式和精度),在x64架构上将它转换为double类型时会自动舍入到最近的double值,而这个近似值恰好比原本的值大了一点点,超出了int64_t的表示范围。于是,把clamped转换为int64_t时就出现了未定义行为。

C++语言标准规定,在将浮点数转换为整数时,会直接丢弃小数部分(隐式转换 – cppreference.com)。不难发现,无论浮点类型精度如何,只要数值小于263且大于等于-263,在舍弃小数部分之后就在int64_t的表示范围内,而常见的计算机架构中采用二进制浮点数(即:std::numeric_limits<double>::radix == 2),在不溢出的前提下能够准确表示2的幂,所以我们可以利用这段示例代码来安全地将double类型浮点数转换为最接近的int64_t数值:

std::optional<int64_t> saturated_to_int64(double source)
{
    if (std::isnan(source))
    {
        return std::nullopt;
    }
    else if (source < -std::exp2(63))
    {
        return std::numeric_limits<int64_t>::min();
    }
    else if (source >= std::exp2(63))
    {
        return std::numeric_limits<int64_t>::max();
    }
    else
    {
        return source;
    }
}

或者,我们还可以使用一种与double类型的具体表示完全无关的方法:利用标准库llrint函数以及浮点异常功能。llrint函数调用时,若参数超出了long long类型的表示范围,则会引发FE_INVALID浮点异常(std::rint – cppreference.com)。示例代码:

std::optional<int64_t> saturated_to_int64(double source)
{
    static_assert(std::is_same_v<long long, int64_t>);
    if (std::isnan(source))
    {
        return std::nullopt;
    }

    fenv_t fenv;
    std::fegetenv(&fenv);
    std::feclearexcept(FE_ALL_EXCEPT);
    std::fesetround(FE_TOWARDZERO);
    int64_t result = std::llrint(source);
    if (std::fetestexcept(FE_INVALID))
    {
        if (source > 0)
        {
            result = std::numeric_limits<int64_t>::max();
        }
        else
        {
            result = std::numeric_limits<int64_t>::min();
        }
    }

    std::fesetenv(&fenv);
    return result;
}

该方法不需要假设double类型的具体精度和二进制表示形式,可以有效防止由于超出范围导致的未定义行为。