Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 625
Ambroz Bizjak Avatar asked Jun 19 '17 16:06

Ambroz Bizjak


Video Answer


2 Answers

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
like image 118
Howard Hinnant Avatar answered Oct 23 '22 06:10

Howard Hinnant


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.

like image 39
Dominic Pöschko Avatar answered Oct 23 '22 06:10

Dominic Pöschko