Is it safe to cast to int from std::round?

Viewed 6993

I have a question about std::round with signature: double round (double x);

Let's say I have this code:

int i = std::round(0.9);

In this case, std::round should return 1.00000000000, but that's uncomfortably close to 0.9999999999999 and I'm concerned that floating-point errors will end up rounding this down.

I would hope that i == 1, but is this guaranteed?

6 Answers

The std::round function returns a floating point value, "rounding halfway cases away from zero". As with any double to int implicit conversion the compiler will issue a warning:

conversion from 'double' to 'int', possible loss of data

Use std::lround or std::lrint if you want to return an integral value.

Yes. So long as you don't overflow the receiving type.

You have no concerns here: std::round(0.9) will round to exactly 1.0 and so i == 1 is guaranteed

The standard insists on the closest integral value assumable by a double being returned.

Note though that for an IEEE754 double, all values over the 52nd power of 2 are integral values! A corollary of that is that your candidate number for rounding is already an integral value, so the function reduces to a no-op. So the fact, for example, that std::round(4503599627370496.5) will return 4503599627370496 is all to do with the fact that 4503599627370496.5 cannot be represented as a double in the first place.

As final technical point, note that std::round is remarkably well-behaved, due in part to the fact that any number of the form a.5 (which is the cutover point in rounding) is a dyadic rational and so can be represented exactly in binary floating point. This is why an alternative approach, such as adding 0.5 and truncating, can introduce bugs since joke digits can be introduced if you do that.

The C++ Standard defers to the C Standard for this. In N1570 (~C11), the description of round is as follows:

The round functions round their argument to the nearest integer value in floating-point format, rounding halfway cases away from zero, regardless of the current rounding direction.

However, as Ron pointed out, functions such as lrint do exactly what you want.

According to cppreference:

Floating–integral conversions

A prvalue of floating-point type can be converted to a prvalue of any integer type. The fractional part is truncated, that is, the fractional part is discarded. If the value cannot fit into the destination type, the behavior is undefined (even when the destination type is unsigned, modulo arithmetic does not apply). If the destination type is bool, this is a boolean conversion (see below).

Although this isn't a proof that it will be true on all platforms, you can just try to round all possible floating point values and see what happens:

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

int main() {
  // Define the range of valid 'int' values
  const float mi = std::numeric_limits<int>::min();
  const float ma = std::numeric_limits<int>::max();

  float maxd = -std::numeric_limits<float>::infinity();
  std::size_t k = 0;
  // Iterate over all float point values between 'mi' and 'ma'
  for (float f = mi; f < ma; f = std::nextafter(f, ma), ++k) {
    // Static cast after round
    auto i = static_cast<int>(std::round(f));
    // Convert back to float and check the difference
    float d = std::abs(static_cast<float>(i) - f);
    // Store the largest recorded difference
    if (d > maxd) {
      maxd = d;
    }
    // Print progress to the console, just to see it's not stuck
    if (k % 1321 == 0) {
      std::cout << k << ": " << f << "               \r";
    }
  }
  std::cout << std::endl;

  // Prints 0.5 for me
  std::cout << maxd << std::endl;
}

If what you feared could happen did happen, the program above would print a value larger than 0.5.

how about adding 0.5 and then casting to int (truncating)

if(x < 0) { //number is negative
 return (int) x - 0.5;
} else {
 return (int) x + 0.5;//number is positive
}

in your example it should return 1 for sure because 1.49999999999 cast to int is 1

Related