hkBattousai
hkBattousai

Reputation: 10931

Does casting `std::floor()` and `std::ceil()` to integer type always give the correct result?

I am being paranoid that one of these functions may give an incorrect result like this:

std::floor(2000.0 / 1000.0) --> std::floor(1.999999999999) --> 1
or
std::ceil(18 / 3) --> std::ceil(6.000000000001) --> 7

Can something like this happen? If there is indeed a risk like this, I'm planning to use the functions below in order to work safely. But, is this really necessary?

constexpr long double EPSILON = 1e-10;

intmax_t GuaranteedFloor(const long double & Number)
{
    if (Number > 0)
    {
        return static_cast<intmax_t>(std::floor(Number) + EPSILON);
    }
    else
    {
        return static_cast<intmax_t>(std::floor(Number) - EPSILON);
    }
}

intmax_t GuaranteedCeil(const long double & Number)
{
    if (Number > 0)
    {
        return static_cast<intmax_t>(std::ceil(Number) + EPSILON);
    }
    else
    {
        return static_cast<intmax_t>(std::ceil(Number) - EPSILON);
    }
}

(Note: I'm assuming that the the given 'long double' argument will fit in the 'intmax_t' return type.)

Upvotes: 16

Views: 8146

Answers (4)

Sneftel
Sneftel

Reputation: 41474

People often get the impression that floating point operations produce results with small, unpredictable, quasi-random errors. This impression is incorrect.

Floating point arithmetic computations are as exact as possible. 18/3 will always produce exactly 6. The result of 1/3 won't be exactly one third, but it will be the closest number to one third that is representable as a floating point number.

So the examples you showed are guaranteed to always work. As for your suggested "guaranteed floor/ceil", it's not a good idea. Certain sequences of operations can easily blow the error far above 1e-10, and certain other use cases will require 1e-10 to be correctly recognized (and ceil'ed) as nonzero.

As a rule of thumb, hardcoded epsilon values are bugs in your code.

Upvotes: 21

Jonathan Lidbeck
Jonathan Lidbeck

Reputation: 1614

As long as the floating-point values x and y exactly represent integers within the limits of the type you're using, there's no problem--x / y will always yield a floating-point value that exactly represents the integer result. Casting to int as you're doing will always work.

However, once the floating-point values go outside the integer-representable range for the type (Representing integers in doubles), epsilons don't help.

Consider this example. 16777217 is the smallest integer not exactly representable as a 32-bit float:

int ix=16777217, iy=97;
printf("%d / %d = %d", ix, iy, ix/iy);
// yields "16777217 / 97 = 172961" which is accurate

float x=ix, y=iy;
printf("%f / %f = %f", x, y, x/y);
// yields "16777216.000000 / 97.000000 = 172960.989691"

In this case, the error is negative; in other cases (try 16777219 / 1549), the error is positive.

While it's tempting to add an epsilon to make floor work, it won't extend the accuracy much. When the values differ by more orders of magnitude, the error becomes greater than 1 and integer-accuracy can't be guaranteed. Specifically, when x/y exceeds the max. representable, the error can exceed 1.0, so the epsilon is no help.

If this is coming into play, you will have to consider changing your mathematical approach--order of operations, work with logarithms, etc.

Upvotes: 1

Hakan Kapson
Hakan Kapson

Reputation: 21

Such results are likely to appear when working with doubles. You can use round or you can subtract 0.5 then use std::ceil function.

Upvotes: -6

Xirema
Xirema

Reputation: 20396

In the specific examples you're listing, I don't think those errors would ever occur.

std::floor(2000.0 /*Exactly Representable in 32-bit or 64-bit Floating Point Numbers*/ / 1000.0 /*Also exactly representable*/) --> std::floor(2.0 /*Exactly Representable*/) --> 2
std::ceil(18 / 3 /*both treated as ints, might not even compile if ceil isn't properly overloaded....?*/) --> 6
std::ceil(18.0 /*Exactly Representable*/ / 3.0 /*Exactly Representable*/) --> 6

Having said that, if you have math that depends on these functions behaving exactly correctly for floating point numbers, that may illuminate a design flaw you need to reconsider/reexamine.

Upvotes: 4

Related Questions