How to properly compare an integer and a floating-point value?

前端 未结 6 1807
遥遥无期
遥遥无期 2021-01-12 12:02

How do I compare an integer and a floating-point value the right way™?

The builtin comparsion operators give incorrect results in some edge cases, for examp

6条回答
  •  梦毁少年i
    2021-01-12 13:00

    Here's what I ended up with.

    Credit for the algorithm goes to @chux; his approach appears to outperform the other suggestions. You can find some alternative implementations in the edit history.

    If you can think of any improvements, suggestions are welcome.

    #include 
    #include 
    #include 
    
    enum partial_ordering {less, equal, greater, unordered};
    
    template 
    partial_ordering compare_int_float(I i, F f)
    {
        if constexpr (std::is_integral_v && std::is_floating_point_v)
        {
            return compare_int_float(f, i);
        }
        else
        {
            static_assert(std::is_integral_v && std::is_floating_point_v);
            static_assert(std::numeric_limits::radix == 2);
    
            // This should be exactly representable as F due to being a power of two.
            constexpr F I_min_as_F = std::numeric_limits::min();
    
            // The `numeric_limits::max()` itself might not be representable as F, so we use this instead.
            constexpr F I_max_as_F_plus_1 = F(std::numeric_limits::max()/2+1) * 2;
    
            // Check if the constants above overflowed to infinity. Normally this shouldn't happen.
            constexpr bool limits_overflow = I_min_as_F * 2 == I_min_as_F || I_max_as_F_plus_1 * 2 == I_max_as_F_plus_1;
            if constexpr (limits_overflow)
            {
                // Manually check for special floating-point values.
                if (std::isinf(f))
                    return f > 0 ? less : greater;
                if (std::isnan(f))
                    return unordered;
            }
    
            if (limits_overflow || f >= I_min_as_F)
            {
                // `f <= I_max_as_F_plus_1 - 1` would be problematic due to rounding, so we use this instead.
                if (limits_overflow || f - I_max_as_F_plus_1 <= -1)
                {
                    I f_trunc = f;
                    if (f_trunc < i)
                        return greater;
                    if (f_trunc > i)
                        return less;
    
                    F f_frac = f - f_trunc;
                    if (f_frac < 0)
                        return greater;
                    if (f_frac > 0)
                        return less;
    
                    return equal;
                }
    
                return less;
            }
    
            if (f < 0)
                return greater;
    
            return unordered;
        }
    }
    

    If you want to experiment with it, here are a few test cases:

    #include 
    #include 
    #include  
    
    void compare_print(long long a, float b, int n = 0)
    {
        if (n == 0)
        {
            auto result = compare_int_float(a,b);
            std::cout << a << ' ' << "<=>?"[int(result)] << ' ' << b << '\n';
        }
        else
        {
            for (int i = 0; i < n; i++)
                b = std::nextafter(b, -INFINITY);
    
            for (int i = 0; i <= n*2; i++)
            {
                compare_print(a, b);
                b = std::nextafter(b, INFINITY);
            }
    
            std::cout << '\n';
        }
    }
    
    int main()
    {    
        std::cout << std::setprecision(1000);
    
        compare_print(999999984306749440,
                      999999984306749440.f, 2);
    
        compare_print(999999984306749439,
                      999999984306749440.f, 2);
    
        compare_print(100,
                      100.f, 2);
    
        compare_print(-100,
                      -100.f, 2);
    
        compare_print(0,
                      0.f, 2);
    
        compare_print((long long)0x8000'0000'0000'0000,
                      (long long)0x8000'0000'0000'0000, 2);
    
        compare_print(42, INFINITY);
        compare_print(42, -INFINITY);
        compare_print(42, NAN);
        std::cout << '\n';
    
        compare_print(1388608,
                      1388608.f, 2);
    
        compare_print(12388608,
                      12388608.f, 2);
    }
    

    (run the code)

提交回复
热议问题