如何将任意大的持续时间转换为纳秒精度持续时间而不会溢出?
How to convert an arbitrarily large duration to a nanosecond precision duration without overflows?
假设我有自己的时钟,其纪元与 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;
};
我需要像这样将任何 tp
(system_clock::time_point
类型)转换为 myclock::time_point
:
(如果需要)截断 tp
以丢弃“过去纳秒”精度
如果tp.time_since_epoch()
在myclock::time_point
的有效范围内--returnmyclock::time_point() + tp.time_since_epoch()
否则抛出异常
但是,不知道 system_clock
s period
和 rep
我 运行 进入 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 );
如何以这样的方式编写此逻辑,使其对任何 tp
和任意 system_clock::period/rep
都能可靠地工作?
P.S。我检查了 MSVC 对 duration_cast
/time_point_cast
的实现,但它们似乎有同样的问题(或需要相同的时钟类型)。
我强烈建议你把这个问题分成两部分:
转换任意精度time_point<myclock, D>
to/fromtime_point<system_clock, D>
,同时保留精度D
.
编写一个自由函数(比如 checked_convert
)以从一种精度转换为另一种精度(在同一个 time_point
时钟系列中)并在溢出时抛出。
第一个 time_point
s 在时钟之间的转换:
像这样将静态成员函数to_sys
和from_sys
添加到myclock
:
struct myclock
{
using rep = std::int64_t;
using period = std::nano;
using duration = std::chrono::duration<rep, period>;
using time_point = std::chrono::time_point<myclock>;
static constexpr bool is_steady = std::chrono::system_clock::is_steady;
static time_point now() noexcept;
template<typename Duration>
static
std::chrono::time_point<std::chrono::system_clock, Duration>
to_sys(const std::chrono::time_point<myclock, Duration>& tp)
{
using Td = std::chrono::time_point<std::chrono::system_clock, Duration>;
return Td{tp.time_since_epoch()};
}
template<typename Duration>
static
std::chrono::time_point<myclock, Duration>
from_sys(const std::chrono::time_point<std::chrono::system_clock, Duration>& tp)
{
using Td = std::chrono::time_point<myclock, Duration>;
return Td{tp.time_since_epoch()};
}
};
现在您可以像这样转换为 system_clock
:
myclock::time_point tp;
auto tp_sys = myclock::to_sys(tp);
或者 myclock::from_sys
走另一条路。
这样做的妙处在于,当您将来迁移到 C++20 时,您可以将语法更改为:
auto tp_sys = std::chrono::clock_cast<std::chrono::system_clock>(tp);
tp = std::chrono::clock_cast<myclock>(tp_sys);
更酷的是,无需进一步更改您的代码,您还可以 clock_cast
to/from:
utc_clock
tai_clock
gps_clock
file_clock
clock_cast
系统将使用您的 to_sys
/from_sys
从 system_clock
to/from 任何其他 std
-定义的时钟,甚至是选择加入 to_sys
/from_sys
系统的另一个用户定义的时钟。
第二:检查转换
template <class Duration, class Clock, class DurationSource>
std::chrono::time_point<Clock, Duration>
checked_convert(std::chrono::time_point<Clock, DurationSource> tp)
{
using namespace std::chrono;
using Tp = time_point<Clock, Duration>;
using TpD = time_point<Clock, duration<long double, typename Duration::period>>;
TpD m = Tp::min();
TpD M = Tp::max();
if (tp < m || tp > M)
throw std::runtime_error("overflow");
return time_point_cast<Duration>(tp);
}
这里的想法是暂时转换为基于浮点数的 time_points 以进行溢出检查。您也可以使用 128 位整数 rep
。 min/max 高得离谱的任何东西。做检查。抛出溢出。如果安全,则转换为所需的积分 rep
.
sys_time<microseconds> tp1 = sys_days{1600y/1/1};
auto tp2 = checked_convert<nanoseconds>(tp1); // throws "overflow"
std::cout << tp2 << '\n';
(我使用 C++20 语法构造上面的 system_clock-based time_points)
有人会问:为什么checked_convert
不是标准提供的?
答:因为它并不完美。 long double
的精度可能(也可能不会)小于 time_point
下的积分 rep
的精度。更好的选择是 128 位整数 rep
,它肯定具有足够的精度。但是有些平台没有 128 位整数类型。甚至某些平台(低级嵌入式)甚至可能没有浮点类型。所以目前这个问题还没有很好的标准解决方案。客户端可以使用多种技术,包括 one in this good answer.
更新
这是 checked_convert
的 duration
版本:
template <class Duration, class Rep, class Period>
Duration
checked_convert(std::chrono::duration<Rep, Period> d)
{
using namespace std::chrono;
using D = duration<long double, typename Duration::period>;
D m = Duration::min();
D M = Duration::max();
if (d < m || d > M)
throw std::runtime_error("overflow");
return duration_cast<Duration>(d);
}
像这样使用时会抛出异常:
constexpr auto x = duration<int64_t, milli>(numeric_limits<int64_t>::max());
constexpr auto y = duration<int8_t, nano>(1);
auto z = checked_convert<decltype(y)>(x);
编辑: 代码更新以处理一些 if constexpr
相关的问题。
这是我想出的 (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");
}
此函数 returns 持续时间的最大值 Dx
可以安全地转换为持续时间 Dy
,如果满足以下条件:
Dx::rep
和 Dy::rep
是整数
Dx
值为非负值
- 转化率微不足道(
num
或 den
为 1
)
现在使用它可以编写一个安全的转换函数:
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");
}
备注:
很确定 if
下的所有内容都可以用简单的 duration_cast<Dy>(dx)
替换,但在检查了 MSVC 的实现后我更喜欢我的。
现在编写时钟之间的安全转换是微不足道的(如果它们共享相同的纪元):
c1::time_point() + safe_duration_cast<c1::duration>(c2::now().time_since_epoch())
...如果时代不同,则需要的只是一个偏移量和额外的检查(以避免回绕)
这对我来说已经足够了 -- 在所有平台上(我关心的)system_clock
满足我的要求。
但是分析非平凡的情况很奇怪 Rxy
-- 在这种情况下(数学上):
y = x * Rxy = x * n // d
,其中:
//
表示“整数除法”(即7 // 2 = 3
)
n和d属于N(自然数,即1,2,3,...
)
gcd(n,d) == 1
(帮助计算溢出)
诀窍是编写一个通用代码,该代码将在任何平台上执行所述计算以获得最大范围的值。为了性能,可以选择在某些 class 值上失败(例如,如果给定平台有 bignum
,我们可以选择忽略它并使用原始类型执行计算)。
这里有多个方面需要考虑:
对于足够小的 x
你可以简单地 运行 这个计算(中间计算可能使用 comon_type_t<X,Y>
或 intmax_t
)
calc 可以重写为:y = x // d * n + (x % d) * n // d
,在这种情况下,确定给定的 x
是否可以安全转换变得非常重要(例如,如果 X
是 int8_t
和 Rxy = 99/100
那么只有 0,1,100,101
可以在不使用更宽的整数类型(可能不存在)的情况下安全地转换)。请注意,使用无符号类型可以扩大我们安全的 class 值(在 uint8_t
中这样做会将 2
和 102
添加到列表中)
一些实现(而不是预先计算safe values
)可能会使用硬件标志(即如果给定的乘法溢出——它将设置一些CPU标志,这将导致 out_of_range
被抛出)
我敢打赌用浮点 rep
类型做这个会很有趣
删除 x has to be non-negative
要求也很有趣...
补充说明
如果 C++ 提供类似的工具来确保安全转换就好了
使用 std::ratio
很酷,但它隐藏了溢出——通常我依靠编译器来警告我可能出现的问题,std::ratio
打破了这一点。你可以很容易地 运行 进入非常 surprising behaviour 并且你不会知道它直到你的程序在你忘记它很久之后遇到这样的值......特别是,如果从外部检索值(文件时间 stamps/etc)
假设我有自己的时钟,其纪元与 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;
};
我需要像这样将任何 tp
(system_clock::time_point
类型)转换为 myclock::time_point
:
(如果需要)截断
tp
以丢弃“过去纳秒”精度如果
tp.time_since_epoch()
在myclock::time_point
的有效范围内--returnmyclock::time_point() + tp.time_since_epoch()
否则抛出异常
但是,不知道 system_clock
s period
和 rep
我 运行 进入 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 );
如何以这样的方式编写此逻辑,使其对任何 tp
和任意 system_clock::period/rep
都能可靠地工作?
P.S。我检查了 MSVC 对 duration_cast
/time_point_cast
的实现,但它们似乎有同样的问题(或需要相同的时钟类型)。
我强烈建议你把这个问题分成两部分:
转换任意精度
time_point<myclock, D>
to/fromtime_point<system_clock, D>
,同时保留精度D
.编写一个自由函数(比如
checked_convert
)以从一种精度转换为另一种精度(在同一个time_point
时钟系列中)并在溢出时抛出。
第一个 time_point
s 在时钟之间的转换:
像这样将静态成员函数to_sys
和from_sys
添加到myclock
:
struct myclock
{
using rep = std::int64_t;
using period = std::nano;
using duration = std::chrono::duration<rep, period>;
using time_point = std::chrono::time_point<myclock>;
static constexpr bool is_steady = std::chrono::system_clock::is_steady;
static time_point now() noexcept;
template<typename Duration>
static
std::chrono::time_point<std::chrono::system_clock, Duration>
to_sys(const std::chrono::time_point<myclock, Duration>& tp)
{
using Td = std::chrono::time_point<std::chrono::system_clock, Duration>;
return Td{tp.time_since_epoch()};
}
template<typename Duration>
static
std::chrono::time_point<myclock, Duration>
from_sys(const std::chrono::time_point<std::chrono::system_clock, Duration>& tp)
{
using Td = std::chrono::time_point<myclock, Duration>;
return Td{tp.time_since_epoch()};
}
};
现在您可以像这样转换为 system_clock
:
myclock::time_point tp;
auto tp_sys = myclock::to_sys(tp);
或者 myclock::from_sys
走另一条路。
这样做的妙处在于,当您将来迁移到 C++20 时,您可以将语法更改为:
auto tp_sys = std::chrono::clock_cast<std::chrono::system_clock>(tp);
tp = std::chrono::clock_cast<myclock>(tp_sys);
更酷的是,无需进一步更改您的代码,您还可以 clock_cast
to/from:
utc_clock
tai_clock
gps_clock
file_clock
clock_cast
系统将使用您的 to_sys
/from_sys
从 system_clock
to/from 任何其他 std
-定义的时钟,甚至是选择加入 to_sys
/from_sys
系统的另一个用户定义的时钟。
第二:检查转换
template <class Duration, class Clock, class DurationSource>
std::chrono::time_point<Clock, Duration>
checked_convert(std::chrono::time_point<Clock, DurationSource> tp)
{
using namespace std::chrono;
using Tp = time_point<Clock, Duration>;
using TpD = time_point<Clock, duration<long double, typename Duration::period>>;
TpD m = Tp::min();
TpD M = Tp::max();
if (tp < m || tp > M)
throw std::runtime_error("overflow");
return time_point_cast<Duration>(tp);
}
这里的想法是暂时转换为基于浮点数的 time_points 以进行溢出检查。您也可以使用 128 位整数 rep
。 min/max 高得离谱的任何东西。做检查。抛出溢出。如果安全,则转换为所需的积分 rep
.
sys_time<microseconds> tp1 = sys_days{1600y/1/1};
auto tp2 = checked_convert<nanoseconds>(tp1); // throws "overflow"
std::cout << tp2 << '\n';
(我使用 C++20 语法构造上面的 system_clock-based time_points)
有人会问:为什么checked_convert
不是标准提供的?
答:因为它并不完美。 long double
的精度可能(也可能不会)小于 time_point
下的积分 rep
的精度。更好的选择是 128 位整数 rep
,它肯定具有足够的精度。但是有些平台没有 128 位整数类型。甚至某些平台(低级嵌入式)甚至可能没有浮点类型。所以目前这个问题还没有很好的标准解决方案。客户端可以使用多种技术,包括 one in this good answer.
更新
这是 checked_convert
的 duration
版本:
template <class Duration, class Rep, class Period>
Duration
checked_convert(std::chrono::duration<Rep, Period> d)
{
using namespace std::chrono;
using D = duration<long double, typename Duration::period>;
D m = Duration::min();
D M = Duration::max();
if (d < m || d > M)
throw std::runtime_error("overflow");
return duration_cast<Duration>(d);
}
像这样使用时会抛出异常:
constexpr auto x = duration<int64_t, milli>(numeric_limits<int64_t>::max());
constexpr auto y = duration<int8_t, nano>(1);
auto z = checked_convert<decltype(y)>(x);
编辑: 代码更新以处理一些 if constexpr
相关的问题。
这是我想出的 (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");
}
此函数 returns 持续时间的最大值 Dx
可以安全地转换为持续时间 Dy
,如果满足以下条件:
Dx::rep
和Dy::rep
是整数Dx
值为非负值- 转化率微不足道(
num
或den
为1
)
现在使用它可以编写一个安全的转换函数:
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");
}
备注:
很确定
if
下的所有内容都可以用简单的duration_cast<Dy>(dx)
替换,但在检查了 MSVC 的实现后我更喜欢我的。现在编写时钟之间的安全转换是微不足道的(如果它们共享相同的纪元):
c1::time_point() + safe_duration_cast<c1::duration>(c2::now().time_since_epoch())
...如果时代不同,则需要的只是一个偏移量和额外的检查(以避免回绕)
这对我来说已经足够了 -- 在所有平台上(我关心的)system_clock
满足我的要求。
但是分析非平凡的情况很奇怪 Rxy
-- 在这种情况下(数学上):
y = x * Rxy = x * n // d
,其中:
//
表示“整数除法”(即7 // 2 = 3
)n和d属于N(自然数,即
1,2,3,...
)gcd(n,d) == 1
(帮助计算溢出)
诀窍是编写一个通用代码,该代码将在任何平台上执行所述计算以获得最大范围的值。为了性能,可以选择在某些 class 值上失败(例如,如果给定平台有 bignum
,我们可以选择忽略它并使用原始类型执行计算)。
这里有多个方面需要考虑:
对于足够小的
x
你可以简单地 运行 这个计算(中间计算可能使用comon_type_t<X,Y>
或intmax_t
)calc 可以重写为:
y = x // d * n + (x % d) * n // d
,在这种情况下,确定给定的x
是否可以安全转换变得非常重要(例如,如果X
是int8_t
和Rxy = 99/100
那么只有0,1,100,101
可以在不使用更宽的整数类型(可能不存在)的情况下安全地转换)。请注意,使用无符号类型可以扩大我们安全的 class 值(在uint8_t
中这样做会将2
和102
添加到列表中)一些实现(而不是预先计算
safe values
)可能会使用硬件标志(即如果给定的乘法溢出——它将设置一些CPU标志,这将导致out_of_range
被抛出)我敢打赌用浮点
rep
类型做这个会很有趣删除
x has to be non-negative
要求也很有趣...
补充说明
如果 C++ 提供类似的工具来确保安全转换就好了
使用
std::ratio
很酷,但它隐藏了溢出——通常我依靠编译器来警告我可能出现的问题,std::ratio
打破了这一点。你可以很容易地 运行 进入非常 surprising behaviour 并且你不会知道它直到你的程序在你忘记它很久之后遇到这样的值......特别是,如果从外部检索值(文件时间 stamps/etc)