Pernoctador
Pernoctador

Reputation: 100

How to properly avoid SIGFPE and overflow on arithmetic operations

I've been trying to create a Fraction class as complete as possible, to learn C++, classes and related stuff on my own. Among other things, I wanted to ensure some level of "protection" against floating point exceptions and overflows.

Objective:

Avoid overflow and floating point exceptions in arithmetic operations found in common operations, expending the least time/memory. If avoiding is not possible, then at least detect it.

Also, the idea is to not cast to some bigger type. That creates a handful of problems (like there might be no bigger type)

Cases I've found:

  1. Overflow on +, -, *, /, pow, root

    Operations are mostly straightforward (a and b are Long):

    • a+b: if LONG_MAX - b > a then there's overflow. (not enough. a or b might be negatives)
    • a-b: if LONG_MAX - a > -b then there's overflow. (Idem)
    • a*b: if LONG_MAX / b > a then there's overflow. (if b != 0)
    • a/b: might thrown SIGFPE if a << b or overflow if b << 0
    • pow(a,b): if (pow(LONG_MAX, 1.0/b) > a then there's overflow.
    • pow(a,1.0/b): Similar to a/b
  2. Overflow on abs(x) when x = LONG_MIN (or equivalent)

    This is funny. Every signed type has a range [-x-1,x] of possible values. abs(-x-1) = x+1 = -x-1 because overflow. This means there is a case where abs(x) < 0

  3. SIGFPE with big numbers divided by -1

    Found when applying numerator/gcd(numerator,denominator). Sometimes gcd returned -1 and I got a floating point exception.

Easy fixes:

  1. On some operations is easy to check for overflow. If that's the case, I can always cast to double (with the risk of loosing precision over big integers). The idea is to find a better solution, without casting.

    In Fraction arithmetics, sometimes I can do extra checking for simplifications: to solve a/b * c/d (co-primes), I can reduce to co-primes a/d and c/b first.

  2. I can always do cascade if's asking if a or b are <0 or > 0. Not the prettiest. Besides that awful choice, I can create a function neg() that will avoid that overflow
    T neg(T x){if (x > 0) return -x; else return x;},
    
  3. I can take abs(x) of gcd and any similar situation (anywhere x > LONG_MIN)

I'm not sure if 2. and 3. are the best solutions, but seems good enough. I'm posting those here so maybe anyone has a better answer.

Ugliest fixes

In most operations I need to do a lot of extra operations to check and avoid overflow. Here is were I'm pretty sure I can learn a thing or two.

Example:

Fraction Fraction::operator+(Fraction f){
    double lcm = max(den,f.den);
    lcm /= gcd(den, f.den);
    lcm *= min(den,f.den);

    // a/c + b/d = [a*(lcm/d) + b*(lcm/c)] / lcm    //use to create normal fractions

    // a/c + b/d = [a/lcm * (lcm/c)] + [b/lcm * (lcm/d)]    //use to create fractions through double

    double p = (double)num;
    p *= lcm / (double)den;
    double q = (double)f.num;
    q *= lcm / (double)f.den;

    if(lcm >= LONG_MAX || (p + q) >= LONG_MAX || (p + q) <= LONG_MIN){
        //cerr << "Aproximating " << num << "/" << den << " + " << f.num << "/" << f.den << endl;
        p = (double)num / lcm;
        p *= lcm / (double)den;
        q = (double)f.num / lcm;
        q *= lcm / (double)f.den;
        return Fraction(p + q);
    }
    else
        return normal(p + q, (long)lcm);
}  

Which is the best way to avoid overflow on these arithmetic operations?


Edit: There are a handfull of questions in this site quite similar, but those are not the same (detect instead of avoid, unsigned instead of signed, SIGFPE in specific no-related situations).

Checking all of them I found some answers that upon modification might be usefull to give a propper answer, like:

uint32_t x, y;
uint32_t value = x + y;
bool overflow = value < x; // Alternatively "value < y" should also work

Other answers are too general and I wonder if there are any answer more specific for the cases I'm looking at.

Upvotes: 0

Views: 1624

Answers (1)

Kamajii
Kamajii

Reputation: 1888

You need to differentiate between floating point operations and integral operations.

Concerning the latter, operations on unsigned types do not normally overflow, except for division by zero which is undefined behaviour by definition IIRC. This is closely related to the fact that C(++) standard mandates a binary representation for unsigned numbers, which virtually makes them a ring.

In contrast, the C(++) standard allows for multiple implementations of signed numbers (sign+magnitude, 1's complement or, most widely used, 2's complement). So signed overflow is defined to be undefined behaviour, possibly to give compiler implementers more freedom to generate efficient code for their target machines. Also this is the reason for your worries with abs(): At least in 2's complement representation, there is no positive number that is equal in magnitude to the largest negative number in magnitude. Refer to CERT rules for elaboration.

On the floating point side SIGFPE has historically been coined for signalling floating point exceptions. However, given the variety of implementations of the arithmetic units in processors nowadays, SIGFPE should be considered a generic signal that reports arithmetic errors. For instance, the glibc reference manual gives a list of possible reasons, explicitely including integral division by zero.

It is worth noting that floating point operations as per ANSI/IEEE Std 754, which is most commonly used today I suppose, are specifically designed to be a kind of error-proof. This means that for example, when an addition overflows it gives a result of infinity and typically sets a flag that you can check later. It is perfectly legal to use this infinite value in further calculations as the floating point operations have been defined for affine arithmetic. This once was meant to allow long running computations (on slow machines) to continue even with intermediate overflows etc. Note that certain operations are forbidden even in affine arithmetic, for example dividing infinity by infinity or subtracting infinity by infinity.

So the bottom line is that floating point computations should not normally cause floating point exceptions. Yet you can have so-called traps which cause SIGFPE (or a similar mechanism) to be triggered whenever the above mentioned flags become raised.

Upvotes: 0

Related Questions