简体   繁体   中英

Reliable overflow detection of floating-point/integer type conversion

Is there a safe way to reliably determine if an integral type T can store a floating-point integer value f (so f == floor(f) ) without any overflow?

Keep in mind that there is no guarantee that the floating point type F is IEC 559 (IEEE 754) compatible, and that signed integer overflow is undefined behavior in C++. I'm interested in a solution which is correct according to the current C++ (C++17 at the writing) standard and avoids undefined behavior .

The following naive approach is not reliable, since there is no guarantee that type F can represent std::numeric_limits<I>::max() due to floating-point rounding.

#include <cmath>
#include <limits>
#include <type_traits>

template <typename I, typename F>
bool is_safe_conversion(F x)
{
    static_assert(std::is_floating_point_v<F>);
    static_assert(std::is_integral_v<I>);

    // 'fmax' may have a different value than expected
    static constexpr F fmax = static_cast<F>(std::numeric_limits<I>::max());

    return std::abs(x) <= fmax; // this test may gives incorrect results
}

Any idea?

Is there a safe way to reliably determine if an integral type T can store a floating-point integer value f?

Yes. The key is to test if f is in the range T::MIN - 0.999... to T::MAX + 0.999... using floating point math - with no rounding issues. Bonus: rounding mode does not apply.

There are 3 failure paths: too big, too small, not-a-number.


The below assumes int/double . I'll leave the C++ template forming for OP.

Forming exact T::MAX + 1 exactly using floating point math is easy as INT_MAX is a Mersenne Number . (We are not talking about Mersenne Prime here.)

Code takes advantage of:
A Mersenne Number divided by 2 with integer math is also a Mersenne Number .
The conversion of a integer type power-of-2 constant to a floating point type can be certain to be exact .

#define DBL_INT_MAXP1 (2.0*(INT_MAX/2+1)) 
// Below needed when -INT_MAX == INT_MIN
#define DBL_INT_MINM1 (2.0*(INT_MIN/2-1)) 

Forming exact T::MIN - 1 is hard as its absolute value is usually a power-of-2 + 1 and the relative precision of the integer type and the FP type are not certain. Instead code can subtract the exact power of 2 and compare to -1.

int double_to_int(double x) {
  if (x < DBL_INT_MAXP1) {
    #if -INT_MAX == INT_MIN
    // rare non-2's complement machine 
    if (x > DBL_INT_MINM1) {
      return (int) x;
    }
    #else
    if (x - INT_MIN > -1.0) {
      return (int) x;
    }
    #endif 
    Handle_Underflow();
  } else if (x > 0) {
    Handle_Overflow();
  } else {
    Handle_NaN();
  }
}

Regarding floating-point types with non-binary radix ( FLT_RADIX != 2 )

With FLT_RADIX = 4, 8, 16 ... , the conversion would be exact too. With FLT_RADIX == 10 , code is at least exact up to a 34-bit int as a double must encode +/-10^10 exactly. So a problem with say a FLT_RADIX == 10 , 64-bit int machine - a low risk. Based on memory, the last FLT_RADIX == 10 in production was over a decade ago.

The integer type is always encoded as 2's complement (most common), 1s' complement, or sign magnitude. INT_MAX is always a power-2-minus-1. INT_MIN is always a - power-2 or 1 more. Effectively, always base 2.

Any idea?

template <typename I, typename F>
constexpr F maxConvertible()
{
    I i = std::numeric_limits<I>::max();
    F f = F(i);
    while(F(i) == f)
    {
        --i;
    }
    return F(i);
}

Due to rounding, we might have got a too large maximum, now downcounting until we get the next representable double being smaller, which should fit into the integral...

Problem left open: This works fine, if conversion to double involves up-rounding; however, even IEEE 754 allows different rounding modes (if rounding to nearest is applied, which should be the most common rounding mode across current hardware, up-rounding will always occur...).

I have not spotted a solution to safely detect down-rounding yet (might add later; at least detecting "rounding to nearest" has already a solution here ), if this occurs, we get some negative falses near the maxima and minima of the integral values, you might consider this "acceptable" for those few exotic architectures actually doing down-rounding.

Independent from up- or down-rounding, there is a special case for signed integrals anyway: Provided the integral number is represented in two's complement and has more bits than the mantissa of the floating point value, then the types minimum value will be representable as floating point value whereas some greater values will not. Catching this case requires special treatment.

This approach uses the definition of floating-point formats in the C (not C++, see first comment) standard. Knowing the number of digits in the significand (provided by numeric_limits::digits ) and the exponent limit (provided by numeric_limits::max_exponent ) allows us to prepare exact values as end points.

I believe it will work in all conforming C++ implementations subject to the modest additional requirements stated in the initial comment. It supports floating-point formats with or without infinities, with ranges wider or narrower than the destination integer format, and with any rounding rules (because it uses only floating-point arithmetic with exactly representable results, so rounding should never be needed).

/*  This code demonstrates safe conversion of floating-point to integer in
    which the input floating-point value is converted to integer if and only if
    it is in the supported domain for such conversions (the open interval
    (Min-1, Max+1), where Min and Max are the mininum and maximum values
    representable in the integer type).  If the input is not in range, an error
    throw and no conversion is performed.  This throw can be replaced by any
    desired error-indication mechanism so that all behavior is defined.

    There are a few requirements not fully covered by the C++ standard.  They
    should be uncontroversial and supported by all reasonable C++
    implementations:

        The floating-point format is as described in C 2011 5.2.4.2.2 (modeled
        by the product of a sign, a number of digits in some base b, and base b
        raised to an exponent).  I do not see this explicitly specified in the
        C++ standard, but it is implied by the characteristics specified in
        std::numeric_limits.  (For example, C++ requires numeric_limits to
        provide the number of base-b digits in the floating-point
        representation, where b is the radix used, which means the
        representation must have base-b digits.)

        The following operations are exact in floating-point.  (All of them
        are elementary operations and have mathematical results that are
        exactly representable, so there is no need for rounding, and hence
        exact results are expected in any sane implementation.)

            Dividing by the radix of the floating-point format, within its
            range.

            Multiplying by +1 or -1.

            Adding or subtracting two values whose sum or difference is
            representable.

        std::numeric_limits<FPType>::min_exponent is not greater than
        -std::numeric_limits<FPType>::digits.  (The code can be modified to
        eliminate this requirement.)
*/


#include <iostream> //  Not needed except for demonstration.
#include <limits>


/*  Define a class to support safe floating-point to integer conversions.

    This sample code throws an exception when a source floating-point value is
    not in the domain for which a correct integer result can be produced, but
    the throw can be replaced with any desired code, such as returning an error
    indication in an auxiliary object.  (For example, one could return a pair
    consisting of a success/error status and the destination value, if
    successful.)

    FPType is the source floating-point type.
    IType is the destination integer type.
*/
template<typename FPType, typename IType> class FPToInteger
{
private:

    /*  Wrap the bounds we need in a static object so it can be easily
        initialized just once for the entire program.
    */
    static class StaticData
    {
    private:

        /*  This function helps us find the FPType values just inside the
            interval (Min-1, Max+1), where Min and Max are the mininum and
            maximum values representable in the integer type).

            It returns the FPType of the same sign of x+s that has the greatest
            magnitude less than x+s, where s is -1 or +1 according to whether x
            is non-positive or positive.
        */
        static FPType BiggestFPType(IType x)
        {
            /*  All references to "digits" in this routine refer to digits in
                base std::numeric_limits<FPType>::radix.  For example, in base
                3, 77 would have four digits (2212).  Zero is considered to
                have zero digits.

                In this routine, "bigger" and "smaller" refer to magnitude.  (3
                is greater than -4, but -4 is bigger than 3.) */

            //  Abbreviate std::numeric_limits<FPType>::radix.
            const int Radix = std::numeric_limits<FPType>::radix;

            //  Determine the sign.
            int s = 0 < x ? +1 : -1;

            //  Count how many digits x has.
            IType digits = 0;
            for (IType t = x; t; ++digits)
                t /= Radix;

            /*  If the FPType type cannot represent finite numbers this big,
                return the biggest finite number it can hold, with the desired
                sign.
            */
            if (std::numeric_limits<FPType>::max_exponent < digits)
                return s * std::numeric_limits<FPType>::max();

            //  Determine whether x is exactly representable in FPType.
            if (std::numeric_limits<FPType>::digits < digits)
            {
                /*  x is not representable, so we will return the next lower
                    representable value by removing just as many low digits as
                    necessary.  Note that x+s might be representable, but we
                    want to return the biggest FPType less than it, which, in
                    this case, is also the biggest FPType less than x.
                */

                /*  Figure out how many digits we have to remove to leave at
                    most std::numeric_limits<FPType>::digits digits.
                */
                digits = digits - std::numeric_limits<FPType>::digits;

                //  Calculate Radix to the power of digits.
                IType t = 1;
                while (digits--) t *= Radix;

                return x / t * t;
            }
            else
            {
                /*  x is representable.  To return the biggest FPType smaller
                    than x+s, we will fill the remaining digits with Radix-1.
                */

                //  Figure out how many additional digits FPType can hold.
                digits = std::numeric_limits<FPType>::digits - digits;

                /*  Put a 1 in the lowest available digit, then subtract from 1
                    to set each digit to Radix-1.  (For example, 1 - .001 =
                    .999.)
                */
                FPType t = 1;
                while (digits--) t /= Radix;
                t = 1-t;

                //  Return the biggest FPType smaller than x+s.
                return x + s*t;
            }
        }

    public:

        /*  These values will be initialized to the greatest FPType value less
            than std::numeric_limits<IType>::max()+1 and the least FPType value
            greater than std::numeric_limits<IType>::min()-1.
        */
        const FPType UpperBound, LowerBound;

        //  Constructor to initialize supporting data for FPTypeToInteger.
        StaticData()
            : UpperBound(BiggestFPType(std::numeric_limits<IType>::max())),
              LowerBound(BiggestFPType(std::numeric_limits<IType>::min()))
        {
            //  Show values, just for illustration.
            std::cout.precision(99);
            std::cout << "UpperBound = " << UpperBound << ".\n";
            std::cout << "LowerBound = " << LowerBound << ".\n";
        }

    } Data;


public:


    FPType value;


    //  Constructor.  Just remember the source value.
    FPToInteger(FPType x) : value(x) {}


    /*  Perform the conversion.  If the conversion is defined, return the
        converted value.  Otherwise, throw an exception.
    */
    operator IType()
    {
        if (Data.LowerBound <= value && value <= Data.UpperBound)
            return value;
        else
            throw "Error, source floating-point value is out of range.";
    }
};


template<typename FPType, typename IType>
    typename FPToInteger<FPType, IType>::StaticData
        FPToInteger<FPType, IType>::Data;


typedef double FPType;
typedef int    IType;


//  Show what the class does with a requested value.
static void Test(FPType x)
{
    try
    {
        IType y = FPToInteger<FPType, IType>(x);
        std::cout << x << " -> " << y << ".\n";
    }
    catch (...)
    {
        std::cout << x << " is not in the domain.\n";
    }
}


#include <cmath>


int main(void)
{
    std::cout.precision(99);

    //  Simple demonstration (not robust testing).
    Test(0);
    Test(0x1p31);
    Test(std::nexttoward(0x1p31, 0));
    Test(-0x1p31-1);
    Test(std::nexttoward(-0x1p31-1, 0));
}

Can you not just do

static_cast<F>(static_cast<I>(x)) == floor(x)

?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM