Suppose I have my own clock with same epoch as system_clock
:
using namespace std;
struct myclock
{
using rep = int64_t;
using period = nano;
using duration = chrono::duration<rep, period>;
using time_point = chrono::time_point<myclock>;
static constexpr bool is_steady = chrono::system_clock::is_steady;
static time_point now() noexcept;
};
I need to convert any tp
(of type system_clock::time_point
) into myclock::time_point
like this:
(if need be) truncate tp
to discard "past-nanosecond" precision
if tp.time_since_epoch()
is in myclock::time_point
s valid range -- return myclock::time_point() + tp.time_since_epoch()
otherwise, throw an exception
But, without knowing system_clock
s period
and rep
I am running into integer overflows:
constexpr auto x = chrono::duration<int64_t, milli>(numeric_limits<int64_t>::max());
constexpr auto y = chrono::duration<int8_t, nano>(1);
constexpr auto z1 = x / nanoseconds(1) * nanoseconds(1); // naive way to discard post-nanosecond part
constexpr auto z2 = y / nanoseconds(1) * nanoseconds(1);
static_assert( x > y );
How to write this logic in such way that it works reliably for any tp
and arbitrary system_clock::period/rep
?
P.S. I checked MSVC's implementation of duration_cast
/time_point_cast
, but it seems that they have same problem (or require same clock type).
Edit: code update to deal with some if constexpr
-related quirks.
Here is what I came up with (godbolt):
template<class Dx, class Dy>
constexpr Dx conv_limit() // noexcept //C++20: change this into consteval
{
// Notes:
// - pretty sure this works only for non-negative x (for example because of integer promotions in expressions used here)
using X = typename Dx::rep;
using Rx = typename Dx::period;
using Y = typename Dy::rep;
using Ry = typename Dy::period;
using Rxy = ratio_divide<Rx, Ry>; // y = x * Rxy, Rxy = Rx / Ry
constexpr X xmax = numeric_limits<X>::max();
constexpr Y ymax = numeric_limits<Y>::max();
static_assert(numeric_limits<X>::is_integer);
static_assert(numeric_limits<Y>::is_integer);
static_assert(xmax > 0); // sanity checks
static_assert(ymax > 0);
static_assert(Rxy::num > 0);
static_assert(Rxy::den > 0);
if constexpr (Rxy::num == 1) // y = x / Rxy::den
{
static_assert(Rxy::den <= xmax); // ensure Rxy::den fits into X
// largest x such that x / Rxy::den <= ymax
constexpr X lim = [&]() -> X { // have to use lambda to avoid compiler complaining about overflow when this branch is unused
if (xmax / Rxy::den <= ymax)
return xmax;
else
return ymax * Rxy::den + (Rxy::den - 1);
}();
// if (x <= lim) --> y = static_cast<Y>(x / static_cast<X>(Rxy::den));
return Dx(lim);
}
else if constexpr (Rxy::den == 1) // y = x * Rxy::num
{
static_assert(Rxy::num <= ymax); // ensure Rxy::num fits into Y
// largest x such that x * Rxy::num <= Ymax
constexpr X lim = (xmax < ymax ? xmax : ymax) / Rxy::num;
// if (x <= lim) --> y = static_cast<Y>(x) * static_cast<Y>(Rxy::num);
return Dx(lim);
}
else
static_assert(!sizeof(Dy*), "not implemented");
}
this function returns maximum value of duration Dx
that can be safely converted to duration Dy
, if following conditions hold:
Dx::rep
and Dy::rep
are integersDx
value is non-negativenum
or den
is 1
)Using this now it is possible to write a safe conversion function:
template<class Dy, class Dx>
constexpr Dy safe_duration_cast(Dx dx)
{
if (dx.count() >= 0 && dx <= conv_limit<Dx, Dy>())
{
using X = typename Dx::rep;
using Rx = typename Dx::period;
using Y = typename Dy::rep;
using Ry = typename Dy::period;
using Rxy = ratio_divide<Rx, Ry>;
if constexpr (Rxy::num == 1)
return Dy( static_cast<Y>(dx.count() / static_cast<X>(Rxy::den)) );
else if constexpr (Rxy::den == 1)
return Dy( static_cast<Y>(dx.count()) * static_cast<Y>(Rxy::num) );
}
throw out_of_range("can't convert");
}
Notes:
pretty sure everything under the if
can be replaced with simple duration_cast<Dy>(dx)
, but after checking MSVC's implementation I prefer mine.
now it is trivial to write a safe conversion between clocks (if they share same epoch):
c1::time_point() + safe_duration_cast<c1::duration>(c2::now().time_since_epoch())
... and if epochs differ all that is needed is an offset and additional check (to avoid wraparounds)
This is already enough for me -- on all platforms (that I care about) system_clock
satisfies my requirements.
But it is curious to analyze case of non-trivial Rxy
-- in this case (mathematically):
y = x * Rxy = x * n // d
, where:
//
means "whole-number division" (i.e. 7 // 2 = 3
)
n and d belong to N (natural numbers, i.e. 1,2,3,...
)
gcd(n,d) == 1
(to help with overflows in calculations)
The trick would be to write a generic code that will perform said calculation on any platform for maximum range of values. Optionally one can elect to fail on certain class of values for the sake of performance (e.g. if given platform has bignum
, we could choose to ignore it and perform calcs using primitive types).
There are multiple aspects here to consider:
for sufficiently small x
you can simply run this calc (potentially using comon_type_t<X,Y>
or intmax_t
for intermediate calcs)
calc can be rewritten as: y = x // d * n + (x % d) * n // d
, in which case figuring out if given x
is safe to convert becomes non-trivial (e.g. if X
is int8_t
and Rxy = 99/100
then only 0,1,100,101
can be safely converted without using wider integer type (which may not exist)). Note that using unsigned type can widen our safe class of values (doing this in uint8_t
adds 2
and 102
to the list)
some implementation (instead of pre-calculating safe values
) may use hardware flags (i.e. if given multiplication overflows -- it will set the some CPU flag, which will lead to out_of_range
being thrown)
I bet doing this with floating point rep
type could be interesting
dropping x has to be non-negative
requirement could be interesting too...
Additional notes
it would be nice if C++ provided similar tools to ensure safe conversions
using std::ratio
is cool, but it hides overflows -- normally I rely on compiler to warn me about possible issues, std::ratio
breaks this. You can easily run into very surprising behaviour and you wouldn't know it until your program encounters such value long after you forgot about it... Especially, if values are retrieved from outside (file time stamps/etc)