Best way of checking if a floating point is an integer

后端 未结 12 2005
陌清茗
陌清茗 2020-12-25 13:07

[There are a few questions on this but none of the answers are particularly definitive and several are out of date with the current C++ standard].

My research shows

12条回答
  •  佛祖请我去吃肉
    2020-12-25 13:40

    Personally I would recommend using the trunc function introduced in C++11 to check if f is integral:

    #include 
    #include 
    
    template
    bool isIntegral(F f) {
        static_assert(std::is_floating_point::value, "The function isIntegral is only defined for floating-point types.");
        return std::trunc(f) == f;
    }
    

    It involves no casting and no floating point arithmetics both of which can be a source of error. The truncation of the decimal places can surely be done without introducing a numerical error by setting the corresponding bits of the mantissa to zero at least if the floating point values are represented according to the IEEE 754 standard.

    Personally I would hesitate to use fmod or remainder for checking whether f is integral because I am not sure whether the result can underflow to zero and thus fake an integral value. In any case it is easier to show that trunc works without numerical error.

    None of the three above methods actually checks whether the floating point number f can be represented as a value of type T. An extra check is necessary.

    The first option actually does exactly that: It checks whether f is integral and can be represented as a value of type T. It does so by evaluating f == (T)f. This check involves a cast. Such a cast is undefined according to §1 in section 4.9 of the C++11 standard "if the truncated value cannot be represented in the destination type". Thus if f is e.g. larger or equal to std::numeric_limits::max()+1 the truncated value will certainly have an undefined behavior as a consequence.

    That is probably why the first option has an additional range check (f >= std::numeric_limits::min() && f <= std::numeric_limits::max()) before performing the cast. This range check could also be used for the other methods (trunc, fmod, remainder) in order to determine whether f can be represented as a value of type T. However, the check is flawed since it can run into undefined behavior: In this check the limits std::numeric_limits::min/max() get converted to the floating point type for applying the equality operator. For example if T=uint32_t and f being a float, std::numeric_limits::max() is not representable as a floating point number. The C++11 standard then states in section 4.9 §2 that the implementation is free to choose the next lower or higher representable value. If it chooses the higher representable value and f happens to be equal to the higher representable value the subsequent cast is undefined according to §1 in section 4.9 since the (truncated) value cannot be represented in the destination type (uint32_t).

    std::cout << std::numeric_limits::max() << std::endl;  // 4294967295
    std::cout << std::setprecision(20) << static_cast(std::numeric_limits::max()) << std::endl;  // 4294967296 (float is a single precision IEEE 754 floating point number here)
    std::cout << static_cast(static_cast(std::numeric_limits::max())) << std::endl;  // Could be for example 4294967295 due to undefined behavior according to the standard in the cast to the uint32_t.
    

    Consequently, the first option would establish that f is integral and representable as uint32_t even though it is not.

    Fixing the range check in general is not easy. The fact that signed integers and floating point numbers do not have a fixed representation (such as two's complement or IEEE 754) according to the standard do not make things easier. One possibility is to write non-portable code for the specific compiler, architecture and types you use. A more portable solution is to use Boost's NumericConversion library:

    #include 
    
    template
    bool isRepresentableAs(F f) {
        static_assert(std::is_floating_point::value && std::is_integral::value, "The function isRepresentableAs is only defined for floating-point as integral types.");
        return boost::numeric::converter::out_of_range(f) == boost::numeric::cInRange && isIntegral(f);
    }
    

    Then you can finally perform the cast safely:

    double f = 333.0;
    if (isRepresentableAs(f))
        std::cout << static_cast(f) << std::endl;
    else
        std::cout << f << " is not representable as uint32_t." << std::endl;
    // Output: 333
    

提交回复
热议问题