Search code examples
c++c++11c++-chrono

is it possible to overload std::chrono::duration_cast?


I have a device that reports time as integer seconds and fractional seconds in clock ticks. For this particular device, the clock operates a 256MHz. I've defined a custom resolution that I use in duration and time_points:

using Res = std::chrono::duration<uint64_t, std::ratio<1, 256'000'000>>;

(unless it is pertinent, I'd rather not debate uint vs int).

This resolution is adequate for representing times since 1970, with overflow around 2763.

Given,

#include <iostream>
#include <chrono>

int main() {
    using Res = std::chrono::duration<uint64_t, std::ratio<1, 256'000'000>>;
    using namespace std::chrono;
    using TimePoint = time_point<system_clock, Res>;

    auto s_now = system_clock::now();

    auto simple = time_point_cast<Res>(s_now);

    auto s_sec = duration_cast<seconds>(s_now.time_since_epoch());
    auto hard = TimePoint(Res(s_sec));
    hard += duration_cast<Res>(s_now.time_since_epoch() - s_sec);

    std::cout << "s_now  " << s_now.time_since_epoch().count() << "\t" << s_sec.count() << "\n"
              << "simple " << simple.time_since_epoch().count() << "\t" << duration_cast<seconds>(simple.time_since_epoch()).count() << "\n"
              << "hard   " << hard.time_since_epoch().count()  << "\t" << duration_cast<seconds>(hard.time_since_epoch()).count() << "\n"
;
    return 0;
}

then simple is not equal to hard. The output might be something like this:

s_now  1628286679505051812  1628286679
simple 121693484773940438   475365174
hard   416841389953293263   1628286679

What happens is that time_point_cast overflows the int Rep for system_clock::duration.

Based on https://stackoverflow.com/a/51226923/9220132 it would seem that this is legal and moral, but it does not taste great. This limitation is a much too easy to produce source of bugs.

If there were a way for me to define the implementation of casting to my very high resolution type such that I protect against overflow, that would be ideal. Knowing to use a custom helper function instead of casting is just asking for trouble, because someone will forget some time and the bug might go unnoticed into production.


Solution

  • I agree you hit overflow. But it isn't because system_clock::rep is int. Indeed your system_clock::rep is int64_t, and yes it does overflow for this operation.

    Your system_clock::period is nano. And the conversion factor to convert nano to std::ratio<1, 256'000'000> is 32/125. And 32 * 1628286679505051812 overflows the common rep : uint64_t.

    There are several ways to avoid the overflow and get the correct answer. And you show one of them. Another way is to use 128 bit arithmetic, which is available on your platform:

    using D = duration<__int128_t>;
    auto s_now = system_clock::now() + D{0};  // s_now::rep is now 128 bits
    

    This changes the output to:

    s_now  1628286679505051812  1628286679
    simple 416841389953293263   1628286679
    hard   416841389953293263   1628286679
    

    Finally, to answer your question: No, don't override duration_cast. Instead create your_cast and do whatever you want with it. C++17 already does this itself by introducing three new "flavors" of duration_cast/time_point_cast which simply change the default rounding mode:

    template<class ToDuration, class Rep, class Period>
      constexpr ToDuration floor(const duration<Rep, Period>& d);
    template<class ToDuration, class Rep, class Period>
      constexpr ToDuration ceil(const duration<Rep, Period>& d);
    template<class ToDuration, class Rep, class Period>
      constexpr ToDuration round(const duration<Rep, Period>& d);
    
    template<class ToDuration, class Clock, class Duration>
      constexpr time_point<Clock, ToDuration> floor(const time_point<Clock, Duration>& tp);
    template<class ToDuration, class Clock, class Duration>
      constexpr time_point<Clock, ToDuration> ceil(const time_point<Clock, Duration>& tp);
    template<class ToDuration, class Clock, class Duration>
      constexpr time_point<Clock, ToDuration> round(const time_point<Clock, Duration>& tp);
    

    Here are example implementations of these functions. Just copy one, choose an appropriate name for your_cast, and implement it however works best for you.