Is the behaviour of std::chrono::round on out-of-range values as intended?

103 views Asked by At

I'm currently rounding a std::chrono::duration<float> to std::chrono::duration<uint32_t>, and when I give it out-of-range values it rounds to 1s instead of 4294967295s.

Looking at the standard, it says

template <class ToDuration, class Rep, class Period>   
constexpr ToDuration round(const duration<Rep, Period>& d);

[...]
Returns: The value t representable in ToDuration that is the closest to d. [...]

Here is my exact code:

#include <chrono>
#include <cstdio>
#include <limits>
#include <cstdint>

int main()
{
        std::chrono::duration<float> seconds{std::numeric_limits<float>::max()};
        printf("float:    %f\n", seconds.count());
        printf("uint32_t: %u\n", std::chrono::round<std::chrono::duration<uint32_t>>(seconds).count());
        printf(" int32_t: %d\n", std::chrono::round<std::chrono::duration<int32_t>>(seconds).count());
        printf("uint64_t: %lu\n", std::chrono::round<std::chrono::duration<uint64_t>>(seconds).count());
        printf(" int64_t: %ld\n", std::chrono::round<std::chrono::duration<int64_t>>(seconds).count());
}

which outputs

float:    340282346638528859811704183484516925440.000000
uint32_t: 1
 int32_t: -2147483647
uint64_t: 9223372036854775809
 int64_t: -9223372036854775807

As you can see, other integer types also behave strangely. Unlike std::lround et al., std::chrono::round doesn't say anything about being undefined if the floating-point input is out of range.

Am I missing anything?

(For context, I compiled this with clang version 14.0.0-1ubuntu1.1 on x86_64, but I first noticed the issue on an ARMv7 system using gcc.)

2

There are 2 answers

10
Howard Hinnant On BEST ANSWER

duration<Rep, Ratio> is a simple, thin wrapper around Rep. In your example Rep is float in the argument to round and uint32_t in the result.

As a thin wrapper, duration does not alter the fundamental behavior of the Rep for things like overflow and conversion. To do so would add overhead when it is often not desirable.

If specific behavior such as overflow checking is desired, chrono facilitates that by allowing duration<safe_int> where safe_int is a hypothetical class type that emulates integral arithmetic but checks for overflow. Real world examples of such libraries exist, and do not require special adaptation to be used with chrono.

For just float and uint32_t as asked about in this question, the undefined behavior results at the float and uint32_t and level, not at the chrono level. Specifically, when converting float to uint32_t, the behavior is undefined if the truncated value in the float can not be represented in the uint32_t.

There is an open question as to whether the standard actually says this or not as discussed in the comments below. To pursue this further, an interested party should submit an LWG issue:

http://cplusplus.github.io/LWG/lwg-active.html#submit_issue

And then the LWG will decide if there is a bug, and if so, how best to remedy it.

Pertinent links:

0
Nicol Bolas On

According to the standard, the answers given by these implementations are incorrect. The standard text you cited makes it clear that this conversion must find the "closest" value to the input duration d. And 1 is not remotely the closest value to the maximum float, let alone any negative number.

The standard text for chrono::round does not require that d be within the range of the output ToDuration::Rep. Nor does it state that standard float-to-int conversions are at play (where out-of-bounds conversions are explicitly UB). Nor could they be used, since those conversions truncate the fractional parts, while round requires finding the closest value.

So calling chrono::round with this value clearly does not provoke UB.

Should the standard permit this in round? That's for others to debate. But as it stands, these implementations of chrono::round (and MSVC's too, in case you're wondering) are unequivocally defective relative to the actual wording of the standard as it currently stands.