'How to check for overflow in duration_cast

I need to convert one kind of std::chrono::duration to another kind but I need to know when such a conversion is not possible because the value would not be representable.

I have not found any facilities in the standard library to check this. The cppreference page does not specify what happens if the value is out of range, only that conversion from floating-point to integer may be undefined behavior (in my case I need to convert from integer to integer).



Solution 1:[1]

There is no one-size-fits-all solution, however a solution that fits many use cases is to use a double-based duration for the range checking. Maybe something like:

#include <chrono>
#include <iostream>
#include <stdexcept>

template <class Duration, class Rep, class Period>
Duration
checked_convert(std::chrono::duration<Rep, Period> d)
{
    using namespace std::chrono;
    using S = duration<double, typename Duration::period>;
    constexpr S m = Duration::min();
    constexpr S M = Duration::max();
    S s = d;
    if (s < m || s > M)
        throw std::overflow_error("checked_convert");
    return duration_cast<Duration>(s);
}

int
main()
{
    using namespace std::chrono;
    std::cout << checked_convert<nanoseconds>(10'000h).count() << "ns\n";
    std::cout << checked_convert<nanoseconds>(10'000'000h).count() << "ns\n";
}

For me this outputs:

36000000000000000ns
libc++abi.dylib: terminating with uncaught exception of type  std::overflow_error: checked_convert

Solution 2:[2]

After adding constexpr to Howards answer I noticed that following

static_assert(checked_convert<nanoseconds>(nanoseconds::max()) == nanoseconds::max());

lead to a compiler error

<source>: In function 'int main()':

<source>:23:68: error: non-constant condition for static assertion

   23 |     static_assert(checked_convert<nanoseconds>(nanoseconds::max()) == nanoseconds::max());

      |                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~

<source>:23:66:   in 'constexpr' expansion of 'checked_convert<std::chrono::duration<long int, std::ratio<1, 1000000000> >, long int, std::ratio<1, 1000000000> >(std::chrono::duration<long int, std::ratio<1, 1000000000> >::max())'

<source>:16:35:   in 'constexpr' expansion of 'std::chrono::duration_cast<std::chrono::duration<long int, std::ratio<1, 1000000000> >, double, std::ratio<1, 1000000000> >(s)'

/opt/compiler-explorer/gcc-9.2.0/include/c++/9.2.0/chrono:200:21:   in 'constexpr' expansion of 'std::chrono::__duration_cast_impl<std::chrono::duration<long int, std::ratio<1, 1000000000> >, std::ratio<1>, double, true, true>::__cast<double, std::ratio<1, 1000000000> >((* & __d))'

<source>:23:68: error: overflow in constant expression [-fpermissive]

https://godbolt.org/z/2bgPPM


Building upon Howards answer I would suggest the following which does not suffer from this problem.

#include <chrono>
#include <iostream>
#include <stdexcept>
#include <type_traits>

template<class Duration, class Rep, class Period>
constexpr auto checked_convert(std::chrono::duration<Rep, Period> d)
  -> std::enable_if_t<!std::is_same_v<Duration, decltype(d)>, Duration> {
    using namespace std::chrono;
    using S       = duration<double, typename Duration::period>;
    constexpr S m = Duration::min();
    constexpr S M = Duration::max();
    S           s = d;
    if(s < m || s > M) { throw std::overflow_error("checked_convert"); }
    return duration_cast<Duration>(s);
}

template<class Duration>
constexpr Duration checked_convert(Duration d) {
    return d;
}

int main() {
    using namespace std::chrono;
    static_assert(checked_convert<nanoseconds>(nanoseconds::max()) == nanoseconds::max());
    std::cout << checked_convert<nanoseconds>(nanoseconds::max()).count() << "ns\n";
    std::cout << checked_convert<nanoseconds>(10'000h).count() << "ns\n";
    std::cout << checked_convert<nanoseconds>(10'000'000h).count() << "ns\n";
}

The second overload ensures that there is no conversion happening when the from and the to Duration are the the same type.


Another way I got rid of the UB was to change Howars Solution to following:

#include <chrono>
#include <iostream>
#include <stdexcept>

template <class Duration, class Rep, class Period>
constexpr Duration
checked_convert(std::chrono::duration<Rep, Period> d)
{
    using namespace std::chrono;
    using S = duration<double, typename Duration::period>;
    constexpr S m = Duration::min();
    constexpr S M = Duration::max();
    S s = d;
    if (s < m || s > M)
        throw std::overflow_error("checked_convert");
    return duration_cast<Duration>(d);
}

int
main()
{
    using namespace std::chrono;
    static_assert(checked_convert<nanoseconds>(nanoseconds::max()) == nanoseconds::max());
    std::cout << checked_convert<nanoseconds>(10'000h).count() << "ns\n";
    std::cout << checked_convert<nanoseconds>(10'000'000h).count() << "ns\n";
}

Notice the change from return duration_cast<Duration>(s) to return duration_cast<Duration>(d). This lets chrono handle the problem when the two Durations are the same, but I´m not sure if the duration_cast with d is valid for the other cases.

Be aware that i have not tested any of this two solution for many cases. It is very likely that there are hiding other cases where there could be an overflow. I´m not versed enough in floating point arithmetic to verify the answer.

Solution 3:[3]

As Howard already said, there is no one-size-fits-all solution. Howards solution has the drawback that the conversion itself is done using double. So if your duration representation is an integral type and your integers are very large, you will lose precision.

Dominics second version uses return duration_cast<Duration>(d) which avoids a conversion to double. However, here the problem is, that you will not catch any internal overflow within the duration_cast implementation. E.g. the Microsoft implementation of duration_cast boils down to a multiplication of the duration with an constant integer followed by a division by another constant integer.

I think, if you want to avoid a conversion to double and also detect internal overflow, you need to implement the conversion completely on your own. So here is my proposal:

template<class _To, class _Rep, class _Period> constexpr _To checked_convert_holger(const std::chrono::duration<_Rep, _Period> &_Dur) {
    using _CF    = std::ratio_divide<_Period, typename _To::period>;
    using _ToRep = typename _To::rep;
    using _CR    = std::common_type_t<_ToRep, _Rep, intmax_t>;
    const auto x = [&]() {
        if constexpr(_CF::den == 1) {
            if constexpr(_CF::num == 1) return _Dur.count();
            else {
                const _CR dur = static_cast<_CR>(_Dur.count()),
                          num = static_cast<_CR>(_CF::num);
                const _CR temp = dur * num;
                if(dur && ((temp / dur) != num)) throw std::overflow_error("checked_convert");
                return temp;
                }
            }
        else {
            if constexpr(_CF::num == 1) return static_cast<_CR>(_Dur.count()) / static_cast<_CR>(_CF::den);
            else {
                const _CR dur = static_cast<_CR>(_Dur.count()),
                          num = static_cast<_CR>(_CF::num);
                const _CR temp = dur * num;
                if(dur && ((temp / dur) != num)) throw std::overflow_error("checked_convert");
                return temp / static_cast<_CR>(_CF::den);
                }
            }
        }();
    if((x < std::numeric_limits<_ToRep>::min()) || (x > std::numeric_limits<_ToRep>::max())) throw std::overflow_error("checked_convert");
    return static_cast<_To>(static_cast<_ToRep>(x));
    }

Note that this only usable for integral duration representations. For other types, you need to write a separate function.

Also check this Compiler Explorer example: https://godbolt.org/z/EnYhjM54s It shows the precision loss in Howards function (the result might look correct, but note that it is actually off by 100ns) and the missed internal overflow in Dominics function. Note that I've only tested this with the MS compiler/lib. Other compilers might behave differently.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Howard Hinnant
Solution 2 Dominic Pöschko
Solution 3 Holger Strauss