Best way of checking if a floating point is an int

2019-02-01 21:49发布

[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 these are the principal methods used to check if a floating point value can be converted to an integral type T.

  1. if (f >= std::numeric_limits<T>::min() && f <= std::numeric_limits<T>::max() && f == (T)f))

  2. using std::fmod to extract the remainder and test equality to 0.

  3. using std::remainder and test equality to 0.

The first test assumes that a cast from f to a T instance is defined. Not true for std::int64_t to float, for example.

With C++11, which one is best? Is there a better way?

12条回答
欢心
2楼-- · 2019-02-01 22:12

Conclusion:

The answer is use std::trunc(f) == f the time difference is insignificant when comparing all these methods. Even if the specific IEEE unwinding code we write in the example below is technically twice is fast we are only talking about 1 nano second faster.

The maintenance costs in the long run though would be significantly higher. So use a solution that is easier to read and understand by the maintainer is better.

Time in microseconds to complete 12,000,000 operations on a random set of numbers:

  • IEEE breakdown:                                               18
  • std::trunc(f) == f                                  32
  • std::floor(val) - val == 0                35
  • ((uint64_t)f) - f) == 0.0                  38
  • std::fmod(val, 1.0) == 0                     87

The Working out of the conclusion.

A floating point number is two parts:

mantissa:      The data part of the value.
exponent:      a power to multiply it by.

sutch that:

   value =  mantissa * (2^exponent)

So the exponent is basically how many binary digits we are going to shift the "binary point" down the mantissa. A positive value shifts it right a negative value shifts it left. If all the digits to the right of the binary point are zero then we have an integer.

If we assume IEEE 754

We should note that this representation the value is normalized so that the most significant bit in the mantissa is shifted to be 1. Since this bit is always set it is not actually stored (the processor knows its there and compensates accordingly).

So:

If the exponent < 0 then you definitely do not have an integer as it can only be representing a fractional value. If the exponent >= <Number of bits In Mantissa> then there is definately no fractual part and it is an integer (though you may not be able to hold it in an ant.

Otherwise we have to do some work. if the exponent >= 0 && exponent < <Number of bits In Mantissa> then you may by representing an integer if the mantissa is all zero in the bottom half (defined below).

Additional as part of the normalization 127 is added to the exponent (so that there are no negative values stored in the 8 bit exponent field).

#include <limits>
#include <iostream>
#include <cmath>

/*
 *  Bit  31      Sign
 *  Bits 30-23   Exponent
 *  Bits 22-00   Mantissa
 */
bool is_IEEE754_32BitFloat_AnInt(float val)
{
    // Put the value in an int so we can do bitwise operations.
    int  valAsInt = *reinterpret_cast<int*>(&val);

    // Remember to subtract 127 from the exponent (to get real value)
    int  exponent = ((valAsInt >> 23) & 0xFF) - 127;

    int bitsInFraction = 23 - exponent;
    int mask = exponent < 0
                    ? 0x7FFFFFFF
                    : exponent > 23
                         ? 0x00
                         : (1 << bitsInFraction) - 1;

    return !(valAsInt & mask);
}
/*
 *  Bit  63      Sign
 *  Bits 62-52   Exponent
 *  Bits 51-00   Mantissa
 */
bool is_IEEE754_64BitFloat_AnInt(double val)
{
    // Put the value in an long long so we can do bitwise operations.
    uint64_t  valAsInt = *reinterpret_cast<uint64_t*>(&val);

    // Remember to subtract 1023 from the exponent (to get real value)
    int  exponent = ((valAsInt >> 52) & 0x7FF) - 1023;

    int bitsInFraction = 52 - exponent;
    uint64_t mask = exponent < 0
                    ? 0x7FFFFFFFFFFFFFFFLL
                    : exponent > 52
                        ? 0x00
                        : (1LL << bitsInFraction) - 1;

    return !(valAsInt & mask);
}

bool is_Trunc_32BitFloat_AnInt(float val)
{
    return (std::trunc(val) - val == 0.0F);
}

bool is_Trunc_64BitFloat_AnInt(double val)
{
    return (std::trunc(val) - val == 0.0);
}

bool is_IntCast_64BitFloat_AnInt(double val)
{
    return (uint64_t(val) - val == 0.0);
}

template<typename T, bool isIEEE = std::numeric_limits<T>::is_iec559>
bool isInt(T f);

template<>
bool isInt<float, true>(float f) {return is_IEEE754_32BitFloat_AnInt(f);}

template<>
bool isInt<double, true>(double f) {return is_IEEE754_64BitFloat_AnInt(f);}

template<>
bool isInt<float, false>(float f) {return is_Trunc_64BitFloat_AnInt(f);}

template<>
bool isInt<double, false>(double f) {return is_Trunc_64BitFloat_AnInt(f);}

int main()
{
    double  x = 16;
    std::cout << x << "=> " << isInt(x) << "\n";

    x = 16.4;
    std::cout << x << "=> " << isInt(x) << "\n";

    x = 123.0;
    std::cout << x << "=> " << isInt(x) << "\n";

    x = 0.0;
    std::cout << x << "=> " << isInt(x) << "\n";

    x = 2.0;
    std::cout << x << "=> " << isInt(x) << "\n";

    x = 4.0;
    std::cout << x << "=> " << isInt(x) << "\n";

    x = 5.0;
    std::cout << x << "=> " << isInt(x) << "\n";

    x = 1.0;
    std::cout << x << "=> " << isInt(x) << "\n";
}

Results:

> ./a.out
16=> 1
16.4=> 0
123=> 1
0=> 1
2=> 1
4=> 1
5=> 1
1=> 1

Running Some Timing tests.

Test data was generated like this:

(for a in {1..3000000};do echo $RANDOM.$RANDOM;done ) > test.data
(for a in {1..3000000};do echo $RANDOM;done ) >> test.data
(for a in {1..3000000};do echo $RANDOM$RANDOM0000;done ) >> test.data
(for a in {1..3000000};do echo 0.$RANDOM;done ) >> test.data

Modified main() to run tests:

int main()
{
    // ORIGINAL CODE still here.
    // Added this trivial speed test.

    std::ifstream   testData("test.data");  // Generated a million random numbers
    std::vector<double>  test{std::istream_iterator<double>(testData), std::istream_iterator<double>()};
    std::cout << "Data Size: " << test.size() << "\n";
    int count1 = 0;
    int count2 = 0;
    int count3 = 0;

    auto start = std::chrono::system_clock::now();
    for(auto const& v: test)
    {   count1 += is_IEEE754_64BitFloat_AnInt(v);
    }
    auto p1 = std::chrono::system_clock::now();
    for(auto const& v: test)
    {   count2 += is_Trunc_64BitFloat_AnInt(v);
    }
    auto p2 = std::chrono::system_clock::now();
    for(auto const& v: test)
    {   count3 += is_IntCast_64BitFloat_AnInt(v);
    }

    auto end = std::chrono::system_clock::now();

    std::cout << "IEEE  " << count1 << " Time: " << std::chrono::duration_cast<std::chrono::milliseconds>(p1 - start).count() << "\n";
    std::cout << "Trunc " << count2 << " Time: " << std::chrono::duration_cast<std::chrono::milliseconds>(p2 - p1).count()    << "\n";
    std::cout << "Int Cast " << count3 << " Time: " << std::chrono::duration_cast<std::chrono::milliseconds>(end - p2).count()   << "\n";    }

The tests show:

> ./a.out
16=> 1
16.4=> 0
123=> 1
0=> 1
2=> 1
4=> 1
5=> 1
1=> 1
Data Size: 12000000
IEEE  6000199 Time: 18
Trunc 6000199 Time: 32
Int Cast 6000199 Time: 38

The IEEE code (in this simple test) seem to beat the truncate method and generate the same result. BUT the amount of time is insignificant. Over 12 Million calls we saw a differince in 14 milli seconds.

查看更多
孤傲高冷的网名
3楼-- · 2019-02-01 22:12

I'd go deep into the IEE 754 standard and keep thinking only in terms of this type and I'll be assuming 64 bit integers and doubles.

The number is a whole number iff:

  1. the number is zero (regardless on the sign).
  2. the number has mantisa not going to binary fractions (regardless on the sing), while not having any undefined digits for least significant bits.

I made following function:

#include <stdio.h>

int IsThisDoubleAnInt(double number)
{
    long long ieee754 = *(long long *)&number;
    long long sign = ieee754 >> 63;
    long long exp = ((ieee754 >> 52) & 0x7FFLL);
    long long mantissa = ieee754 & 0xFFFFFFFFFFFFFLL;
    long long e = exp - 1023;
    long long decimalmask = (1LL << (e + 52));
    if (decimalmask) decimalmask -= 1;
    if (((exp == 0) && (mantissa != 0)) || (e > 52) || (e < 0) || ((mantissa & decimalmask) != 0))
    {
        return 0;
    }
    else
    {
        return 1;
    }
}

As a test of this function:

int main()
{
    double x = 1;
    printf("x = %e is%s int.\n", x, IsThisDoubleAnInt(x)?"":" not");
    x = 1.5;
    printf("x = %e is%s int.\n", x, IsThisDoubleAnInt(x)?"":" not");
    x = 2;
    printf("x = %e is%s int.\n", x, IsThisDoubleAnInt(x)?"":" not");
    x = 2.000000001;
    printf("x = %e is%s int.\n", x, IsThisDoubleAnInt(x)?"":" not");
    x = 1e60;
    printf("x = %e is%s int.\n", x, IsThisDoubleAnInt(x)?"":" not");
    x = 1e-60;
    printf("x = %e is%s int.\n", x, IsThisDoubleAnInt(x)?"":" not");
    x = 1.0/0.0;
    printf("x = %e is%s int.\n", x, IsThisDoubleAnInt(x)?"":" not");
    x = x/x;
    printf("x = %e is%s int.\n", x, IsThisDoubleAnInt(x)?"":" not");
    x = 0.99;
    printf("x = %e is%s int.\n", x, IsThisDoubleAnInt(x)?"":" not");
    x = 1LL << 52;
    printf("x = %e is%s int.\n", x, IsThisDoubleAnInt(x)?"":" not");
    x = (1LL << 52) + 1;
    printf("x = %e is%s int.\n", x, IsThisDoubleAnInt(x)?"":" not");
}

The result is following:

x = 1.000000e+00 is int.
x = 1.500000e+00 is not int.
x = 2.000000e+00 is int.
x = 2.000000e+00 is not int.
x = 1.000000e+60 is not int.
x = 1.000000e-60 is not int.
x = inf is not int.
x = nan is not int.
x = 9.900000e-01 is not int.
x = 4.503600e+15 is int.
x = 4.503600e+15 is not int.

The condition in the method is not very clear, thus I'm posting the less obfuscated version with commented if/else structure.

int IsThisDoubleAnIntWithExplanation(double number)
{
    long long ieee754 = *(long long *)&number;
    long long sign = ieee754 >> 63;
    long long exp = ((ieee754 >> 52) & 0x7FFLL);
    long long mantissa = ieee754 & 0xFFFFFFFFFFFFFLL;
    if (exp == 0)
    {
        if (mantissa == 0)
        {
            // This is signed zero.
            return 1;
        }
        else
        {
            // this is a subnormal number
            return 0;
        }
    }
    else if (exp == 0x7FFL)
    {
        // it is infinity or nan.
        return 0;
    }
    else
    {
        long long e = exp - 1023;
        long long decimalmask = (1LL << (e + 52));
        if (decimalmask) decimalmask -= 1;
        printf("%f: %llx (%lld %lld %llx) %llx\n", number, ieee754, sign, e, mantissa, decimalmask);
        // number is something in form (-1)^sign x 2^exp-1023 x 1.mantissa
        if (e > 63)
        {
            // number too large to fit into integer
            return 0;
        }
        else if (e > 52)
        {
            // number too large to have all digits...
            return 0;
        }
        else if (e < 0)
        {
            // number too large to have all digits...
            return 0;
        }
        else if ((mantissa & decimalmask) != 0)
        {
            // number has nonzero fraction part.
            return 0;
        }
    }
    return 1;
}
查看更多
叼着烟拽天下
4楼-- · 2019-02-01 22:12

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

#include <cmath>
#include <type_traits>

template<typename F>
bool isIntegral(F f) {
    static_assert(std::is_floating_point<F>::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<T>::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<T>::min() && f <= std::numeric_limits<T>::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<T>::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<T>::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<uint32_t>::max() << std::endl;  // 4294967295
std::cout << std::setprecision(20) << static_cast<float>(std::numeric_limits<uint32_t>::max()) << std::endl;  // 4294967296 (float is a single precision IEEE 754 floating point number here)
std::cout << static_cast<uint32_t>(static_cast<float>(std::numeric_limits<uint32_t>::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 <boost/numeric/conversion/cast.hpp>

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

Then you can finally perform the cast safely:

double f = 333.0;
if (isRepresentableAs<uint32_t>(f))
    std::cout << static_cast<uint32_t>(f) << std::endl;
else
    std::cout << f << " is not representable as uint32_t." << std::endl;
// Output: 333
查看更多
Rolldiameter
5楼-- · 2019-02-01 22:12

Here is what I would try:

float originalNumber;
cin >> originalNumber;
int temp = (int) originalNumber;
if (originalNumber-temp > 0)
{
    // It is not an integer
}
else
{
    // It is an integer
}
查看更多
时光不老,我们不散
6楼-- · 2019-02-01 22:17

First of all, I want to see if I got your question right. From what I've read, it seems that you want to determine if a floating-point is actually simply a representation of an integral type in floating-point.

As far as I know, performing == on a floating-point is not safe due to floating-point inaccuracies. Therefore I am proposing the following solution,

template<typename F, typename I = size_t>
bool is_integral(F f)
{
  return fabs(f - static_cast<I>(f)) <= std::numeric_limits<F>::epsilon;
}

The idea is to simply find the absolute difference between the original floating-point and the floating-point casted to the integral type, and then determine if it is smaller than the epsilon of the floating-point type. I'm assuming here that if it is smaller than epsilon, the difference is of no importance to us.

Thank you for reading.

查看更多
干净又极端
7楼-- · 2019-02-01 22:17

Use modf() which breaks the value into integral and fractional parts. From this direct test, it is known if the double is a whole number or not. After this, limit tests against the min/max of the target integer type can be done.

#include <cmath>

bool IsInteger(double x) {
  double ipart;
  return std::modf(x, &ipart) == 0.0;  // Test if fraction is 0.0.
}

Note modf() differs from the similar named fmod().

Of the 3 methods OP posted, the cast to/from an integer may perform a fair amount of work doing the casts and compare. The other 2 are marginally the same. They work, assuming no unexpected rounding mode effects from dividing by 1.0. But do an unnecessary divide.

As to which is fastest likely depends on the mix of doubles used.

OP's first method has a singular advantage: Since the goal is to test if a FP may convert exactly to a some integer, and likely then if the result is true, the conversion needs to then occur, OP's first method has already done the conversion.

查看更多
登录 后发表回答