Search code examples
c++type-conversionimplicit-conversionc++-chrono

Should I use high-resolution time types as parameters to avoid duration_casts?


I read that if the highest resolution type your program uses is, say, milliseconds and this type will cover the whole range your values are in, you should stick with it mostly, to avoid having duration_casts everywhere. Instead you would have only one, inside the function, if all you need is a precision of, let's say, seconds.

However, for return types of functions you would conversely return the lowest resolution possible, to have the lowest possible amount of duration_casts (and here even optimal performance, since you would do the conversion anyway in this case).

Unfortunately I didn't find a source that covers this issue comprehensively (or really, in any way). Is this the way std::chrono should be used (assuming one is not eager about the last possible bit of performance)?


Solution

  • I don't think there's a lot one can say in general about this strategy except that yes, you don't want to needlessly litter your code with duration_cast. Reserve duration_cast only for those conversions where you want to truncate the precision. And in those cases recognize that duration_cast truncates towards zero. C++17 introduces 3 other truncation strategies:

    • floor: truncate towards negative infinity
    • ceil: truncate towards positive infinity
    • round: truncate towards nearest, towards even on tie

    (If you are in C++11/14 you can grab these handy utilities here).

    And certainly if milliseconds is the finest precision you want to deal with, taking milliseconds as parameter types is a good strategy.

    Using a coarser precision for return types might be good for some applications, but probably not for all. Consider:

    seconds process(milliseconds input);
    

    process is some function that takes a duration input, performs some operation on it, and returns the duration output. And in this example I've chosen a coarser precision than the input (seconds). Presumably, at some point I've truncated the precision of the input, and in the process lost information.

    Now if the purpose of process is to truncate precision, this is perfectly fine. But if the purpose of process is something else and you're just returning seconds so that clients can do either of:

    milliseconds result1 = process(input);
    seconds      result2 = process(input);
    

    then you may not be doing the client any favors. Certainly the syntax for obtaining result2 is very clean. But you've also taken away the choice of this client on the direction of truncation (down, up, nearest, etc.). Maybe that is a good thing for your application. But it won't be a good thing for everyone, and thus not for a general purpose library.

    As another example, lets look at the return type of one of the functions in <chrono> itself:

    template<class Rep1, class Period1, class Rep2, class Period2>
    constexpr
    common_type_t<duration<Rep1, Period1>, duration<Rep2, Period2>>
    operator+(const duration<Rep1, Period1>& lhs,
              const duration<Rep2, Period2>& rhs);
    

    In this example the function (operator+) returns the finest precision from its inputs as opposed to the coarsest, in an effort to not loose any information. The client is free to truncate that result if desired. For those who aren't familiar with common_type_t, this is consistent with:

    constexpr
    milliseconds
    operator+(const seconds& lhs, const milliseconds& rhs);