Search code examples
c++c++17roundingundefined-behaviorc++-chrono

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


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.)


Solution

  • 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: