如何将任意大的持续时间转换为纳秒精度持续时间而不会溢出?

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;
};

我需要像这样将任何 tpsystem_clock::time_point 类型)转换为 myclock::time_point

  1. (如果需要)截断 tp 以丢弃“过去纳秒”精度

  2. 如果tp.time_since_epoch()myclock::time_point的有效范围内--returnmyclock::time_point() + tp.time_since_epoch()

  3. 否则抛出异常

但是,不知道 system_clocks periodrep 我 运行 进入 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 的实现,但它们似乎有同样的问题(或需要相同的时钟类型)。

我强烈建议你把这个问题分成两部分:

  1. 转换任意精度time_point<myclock, D>to/fromtime_point<system_clock, D>,同时保留精度D.

  2. 编写一个自由函数(比如 checked_convert)以从一种精度转换为另一种精度(在同一个 time_point 时钟系列中)并在溢出时抛出。

第一个 time_points 在时钟之间的转换:

像这样将静态成员函数to_sysfrom_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:

  1. utc_clock
  2. tai_clock
  3. gps_clock
  4. file_clock

clock_cast 系统将使用您的 to_sys/from_syssystem_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_convertduration 版本:

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::repDy::rep 是整数
  • Dx 值为非负值
  • 转化率微不足道(numden1

现在使用它可以编写一个安全的转换函数:

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 是否可以安全转换变得非常重要(例如,如果 Xint8_tRxy = 99/100 那么只有 0,1,100,101 可以在不使用更宽的整数类型(可能不存在)的情况下安全地转换)。请注意,使用无符号类型可以扩大我们安全的 class 值(在 uint8_t 中这样做会将 2102 添加到列表中)

  • 一些实现(而不是预先计算safe values)可能会使用硬件标志(即如果给定的乘法溢出——它将设置一些CPU标志,这将导致 out_of_range 被抛出)

  • 我敢打赌用浮点 rep 类型做这个会很有趣

  • 删除 x has to be non-negative 要求也很有趣...

补充说明

  • 如果 C++ 提供类似的工具来确保安全转换就好了

  • 使用 std::ratio 很酷,但它隐藏了溢出——通常我依靠编译器来警告我可能出现的问题,std::ratio 打破了这一点。你可以很容易地 运行 进入非常 surprising behaviour 并且你不会知道它直到你的程序在你忘记它很久之后遇到这样的值......特别是,如果从外部检索值(文件时间 stamps/etc)