C++11:chrono

C++11 新增了一个和时间日期相关的模块chrono,位于头文件<chrono>中,用来处理有关时间日期的表示和计算,功能十分强大。它主要包括以下三个概念:

  • 时间段(Durations):顾名思义就是一段时间,比如十毫秒,两分钟,三个小时等等。
  • 时间点(Time points):表示具体一个时间,比如2016年9月24号上午11点整。
  • 时钟(Clocks):和实际时间相关的时钟,比如当前系统的时钟。

需要注意,<chrono> 的相关类和函数并不是定义在 namespace std 中,而是在 namespace std::chrono 中。

时间段 Durations

我们所说的“一段时间”的概念,包括了两个部分:数量和周期单位,比如 100 毫秒这一段时间,单位为毫秒,数量为 100,即 100 个周期单位,一个周期为 1 毫秒。在很多外文资料中,这个数量通常又被称为 Tick,周期单位称为 Period,100 毫秒即为 100 个 tick,每个 tick 的 Peroid 为 1 毫秒。

在 C++11 中,时间段使用 std::chrono::duration 类来表示。它的定义简化后大概如下:

template < class Rep, class Period = ratio<1> >
class duration
{
private:
    Rep _count;
public:
    explicit duration(const Rep&; val) : _count(val) { }
    Rep count() const { return _count; }
};

其中,模板参数 Period 即为周期单位,它使用 std::ratio 类型来表示。而这里的 ratio,又以 1 秒为基准,比如 ratio<1> 表示周期为 1 秒,ratio<60> 表示周期为 60 秒即 1 分钟,ratio<1,1000> 表示周期为千分之一秒,即 1 毫秒,以此类推。

时间段的数量由成员变量 _count 表示,也就该时间段包含多少个周期单位,它需要在构造时指定,并可以通过成员函数 count() 获取。而模板参数 Rep 是用来表示这个数量的类型,通常设为整形,比如 int 或者 long long

来看下面这个例子:

#include <chrono>
#include <iostream>

int main()
{
    using namespace std;
    using namespace std::chrono;

    typedef duration<int, ratio<1>> sec_t;  // 表示秒的类型
    typedef duration<int, ratio<60>> min_t; // 表示分钟的类型

    // or
    // using sec_t = duration<int, ratio<1>>;
    // using min_t = duration<int, ratio<60>>;

    sec_t ten_seconds(10); // 10秒
    min_t two_minutes(2);  // 2分钟

    std::cout << ten_seconds.count() << std::endl; // 10
    std::cout << two_minutes.count() << std::endl; // 2

    return 0;
}

因为 duration 是一个类模板,为了方便使用,通常会先定义好一些类型别名。在这里我们定义了两个表示时间段的类型,sec_tmin_t,分别表示秒和分钟。然后实例化了两个对象 ten_secondstwo_minutes 来分别表示十秒和两分钟。这里的 sec_tmin_t 都是使用 int 类型来存储数据,当然,如果你需要表示一个非常大的时间段,也可以定义为使用 long long 来表示。

可以看出,当模板参数 Rep 为一个整型类型时,Period 实际上就是该 duration 代表时间段类型的精度,sec_t 的表示精度就是1秒,min_t 的表示精度就是1分钟,你不可能使用 sec_t 来表示出毫秒的概念,也不可能使用 min_t 表示出秒的概念。这种精度的规则仅限于 Rep 为一个整型类型时,理论上你确实可以把 Rep 设置为 double 类型,从而可以用 min_t 表示出 0.5 分钟甚至零点几秒的时间段,但一般情况下不太推荐这么做,应当通过调整 ratio 的参数来调整时间段的表示精度。

为了方便使用,在 <chrono> 中其实已经预定义好了几种常见的时间单位类型:

using nanoseconds  = duration<long long, ratio<1, 1000000000>>; // 纳秒
using microseconds = duration<long long, ratio<1, 1000000>>;    // 微秒
using milliseconds = duration<long long, ratio<1, 1000>>;       // 毫秒
using seconds      = duration<long long, ratio<1>>; // 秒
using minutes      = duration<int, ratio<60>>;      // 分钟
using hours        = duration<int, ratio<3600>>;    // 小时

不同单位的时间段类型可以通过 std::chrono::duration_cast 来互相转换,但如果转换后的时间精度不足以表示,精度就会丢失。例如:

#include <chrono>
#include <iostream>

int main()
{
    using namespace std;
    using namespace std::chrono;

    auto p1 = chrono::seconds(5); // 5秒
    auto p2 = chrono::milliseconds(2500); // 2500毫秒

    auto p3 = duration_cast<chrono::milliseconds>(p1); // 5000毫秒
    auto p4 = duration_cast<chrono::seconds>(p2);      // 2秒,精度丢失

    std::cout << p3.count() << std::endl; // 5000
    std::cout << p4.count() << std::endl; // 2

    return 0;
}

实际上,很多情况下也可以不使用 duration_castduration 不同类型之间是可以进行隐式转换的,例如:

auto p1 = chrono::hours(5);
chrono::seconds p2 = p1;
chrono::milliseconds p3 = p1;
p3 = chrono::seconds(100);

duration 也支持基本的运算操作,包括加减乘除,自增自减,和比较运算。看下面的例子:

#include <chrono>
#include <iostream>

int main()
{
    using namespace std;
    using namespace std::chrono;

    auto p1 = chrono::seconds(2);        // 2s
    auto p2 = chrono::milliseconds(500); // 500ms

    chrono::milliseconds p3 = p1 + p2;   // 2500ms
    auto p4 = p1 - p2;                   // 1500ms

    std::cout << p3.count() << std::endl;
    std::cout << p4.count() << std::endl;

    p1 = p1 * 4; // 8s
    std::cout << p1.count() << std::endl;
    p1 = p1 / 2; // 4s
    std::cout << p1.count() << std::endl;
    p1--;        // 3s
    std::cout << p1.count() << std::endl;

    std::cout << std::boolalpha;
    std::cout << (p3 > p4) << std::endl; // true
    std::cout << (p2 > p1) << std::endl; // false

    return 0;
}

duration 可以和另一个 duration 对象相加减,不同精度单位的对象也可以,就比如上例中的 p1p2,但其实会先自动统一成两者中精度高的那一种,因此运算结果的类型会是两者中精度高的。duration 可以乘以或者除以一个整形,表示该时间段的倍数。duration 对象之间也可以进行比较,包括不同精度对象之间的比较也可以。

时钟 Clocks

时钟和时间点的概念联系比较紧密,我们先来介绍时钟的内容。

首先需要明确时钟也具有一个精度的属性。想象一下常见的挂钟和机械手表,基本表盘上都会有个秒针,它们所能体现的最大精度就是秒,但也可能一些挂钟没有秒针,那它的精度就是分,又比如很多运动手表都能显示出毫秒的单位,那它的精度就是毫秒。

chrono 中定义了三种类型的时钟:system_clocksteady_clockhigh_resolution_clock。我们先来介绍 system_clock

system_clock

system_clock 用来表示当前的系统时钟,也就是系统上的真实时间。它的定义大概如下:

struct system_clock
{
    typedef long long                         rep;
    typedef ratio<1, 1000000000>              period; // 1纳秒
    typedef chrono::duration<rep, period>     duration;
    typedef chrono::time_point<system_clock>  time_point;

    static time_point now();
    static std::time_t to_time_t(const time_point&; time);
    static time_point from_time_t(std::time_t tm);

    static constexpr bool is_steady = false;
};

可以看到在它的结构内定义有四个类型属性,其中 system_clock::period 就是这个 system_clock 的精度,同样使用 std::ratio 类型来表示,这个精度在不同的环境中可能存在差异,在笔者所用的 GCC 环境中,该精度为 1 纳秒,但在 MSVC 环境中,该精度为 100 纳秒。

剩下的三个类型属性中:

system_clock::rep 为该时钟内部数据的存储类型,与前面提到的 duration 的模板参数 Rep 类似。 system_clock::duration 为这个时钟上所能表示的时间段类型。 system_clock::time_point 为这个时钟上所能表示的时间点的类型,它的类型为 chrono::time_point,这个类型在后面会详细解释,这里暂时只需要知道用它来表示该时钟上的时间点。

system_clock 提供了三个静态函数:

now(),它返回一个 system_clock::time_point 类型的对象,用来获取当前的时间点。

to_time_t(),它可以将一个 system_clock::time_point 对象转换为 time_t 类型,如果我们需要打印输出一个 system_clock::time_point 时间点信息,就需要通过这个函数先将其转换为 time_t 类型再进行打印。

from_time_t() 则与 to_time_t 相反,它可以把一个 time_t 类型转换为 time_point 类型。

来看下面这个简单例子:

#include <chrono>
#include <iostream>
#include <ctime>

int main ()
{
    using namespace std::chrono;
    auto now = system_clock::now();
    time_t tt = system_clock::to_time_t(now);
    std::cout << ctime(&;tt) << std::endl;
    return 0;
}

system_clock 上还定义了一个 is_steady 的静态类型,对于 system_clock 这个类型的时钟来说,它为 false,我们稍后再解释它的含义。

steady_clock

先再次看一下 system_clock 的定义,在最后面它包含一个 is_steady 的静态常量,并且值为 false,这个常量表示 system_clock 不是一个“稳定的”时钟。这里的“稳定”指的是时钟只能顺时针往前走,不能逆时针往后倒,也就是说,在一个程序内,先由 system_clock::now() 获取的时间点必须始终早于之后再次由 system_clock::now() 获取的时间点。但实际上,system_clock 所代表的是系统时间,如果在程序运行过程中人为的修改系统时间,就不能满足这个“稳定”的要求,因此system_clock 不是稳定的。也正是由于这一点,如果想计算某段代码执行所需要的时间,就不能使用 system_clock

所以,chrono 提供了另外一个时钟类型:steady_clock,而它就是一种“稳定的”时钟。先来看一下它的定义:

struct steady_clock
{
    typedef long long                         rep;
    typedef ratio<1, 1000000000>              period;
    typedef chrono::duration<rep, period>     duration;
    typedef chrono::time_point<system_clock>  time_point;

    static time_point now();

    static constexpr bool is_steady = false;
};

可以看到,在它的结构能同样定义了 rep, period, duration, time_point 四种类型,它们的含义和 system_clock 内部的类型相同,这里不再赘述。其中 period 精度的定义在不同环境中有可能不同,在同一环境中与 system_clock 精度定义也可能不同。

steady_clock 内同样定义了一个 is_steady 静态常量,值为 true,表示它是一个稳定的时钟。

静态函数 steady_clock::now() 用来获取该时钟的当前时间点。但与 system_clock 相比,steady_clock 少了 to_time_tfrom_time_t 两个函数,这是因为,虽然 steady_clock 是一个稳定的时钟类型,但它是一个虚拟的时钟,它不对应系统的时钟,它上面的时间点和实际时间也没有对应关系。

其实 steady_clock 就是为了计算程序执行时间间隔而设计的,如果想获取实际的系统时间,应当使用 system_clock,而如果想计算程序执行时间,应当使用 steady_clock,这一点其实类似于传统的 time.htime()clock() 的区别。

来看 steady_clock 的应用例子:

#include <chrono>
#include <iostream>

int main ()
{
    using namespace std;
    using namespace std::chrono;

    steady_clock::time_point t1 = steady_clock::now();

    for (int i = 0; i < 1000; ++i)
        std::cout << "Hi, there." << std::endl;

    steady_clock::time_point t2 = steady_clock::now();

    chrono::nanoseconds time_span = duration_cast<chrono::nanoseconds>(t2 - t1);
    std::cout << time_span.count() << " nanoseconds." << std::endl;
    return 0;
}

从这个例子中可以看出,要计算时间间隔,只需要将两个时间点相减即可。

high_resolution_clock

除了 system_clocksteady_clock 以外,chrono 中还提供了一种 high_resolution_clock,它表示当前环境中所能提供的最高精度的时钟,但标准并未规定它应该达到多大的精度,或者它是否要稳定。基本上它就是 system_clock 或者 steady_clock 的别名。

using high_resolution_clock = system_clock;
// Or using high_resolution_clock = steady_clock;

通常情况下,直接使用 steady_clocksystem_clock 就可以了,计算时间过了多久用 steady_clock,获取实际时间用system_clock,一般用不着 high_resolution_clock

时间点 Time points

时间点通过 chrono::time_point 类来表示,时间点的概念是和时钟相关的,表示的是某个时钟上的某个时间点,这一点也体现在 chrono::time_point 的结构定义上,它的简化定义如下:

template <class Clock, class Duration = typename Clock::duration>
class time_point
{
public:
    using clock    = Clock;
    using duration = Duration;
    using rep      = typename Duration::rep;
    using period   = typename Duration::period;

    Duration time_since_epoch() const { return _d; }

private:
    Duration _d { Duration::zero() }; // duration since the epoch
};

其中,模板参数 Clock 就是其所关联的时钟类型。回头看一下前面 system_clock 结构中 time_point 类型的定义,就容易理解了。

struct system_clock
{
    ...
    typedef chrono::time_point<system_clock> time_point;
    ...
};

time_point 其实是通过内部的一个时间段变量 _d 来表示一个具体的时间点,这个时间段成员 _d 表示的是从时钟的原点(epoch)开始,到现在所经过的时间长度。对于 system_clock 来说,这个时钟原点通常就是1970年1月1日,对于 steady_clock 来说,通常是系统上次启动的时间,或者程序开始运行的时间。成员函数 time_since_epoch() 用来获取自epoch开始到这个时间点的长度。通过默认函数构造的 time_point 对象就表示的是epoch时间。来看下面这个例子:

#include <chrono>
#include <iostream>
#include <ctime>

int main()
{
    using namespace std;
    using namespace std::chrono;

    system_clock::time_point epoch;
    time_t t_epoch = system_clock::to_time_t(epoch);
    std::cout << "epoch: " << ctime(&;t_epoch);

    system_clock::time_point now = system_clock::now();
    time_t t_now = system_clock::to_time_t(now);
    std::cout << "now: " << ctime(&;t_now);

    std::cout << duration_cast<chrono::hours>(now.time_since_epoch()).count() << " hours since epoch" << std::endl;

    using day_t = duration<int, ratio<24*60*60>>;
    std::cout << duration_cast<day_t>(now.time_since_epoch()).count() << " days since epoch"<< std::endl;

    return 0;
}

时间点 time_point 也支持基本的运算,它可以和一个时间段相加减,得到向前或向后移动的时间点,两个时间点可以相减,得到它们的时间差,时间点之间也可以进行比较。参考下面这个例子:

#include <iostream>
#include <chrono>

int main()
{
    using namespace std::chrono;

    system_clock::time_point tp, tp2;        // epoch value
    system_clock::duration dtn(seconds(1));  // 1 second

                        //  tp     tp2    dtn
                        //  ---    ---    ---
    tp += dtn;          //  e+1s   e      1s
    tp2 -= dtn;         //  e+1s   e-1s   1s
    tp2 = tp + dtn;     //  e+1s   e+2s   1s
    tp = dtn + tp2;     //  e+3s   e+2s   1s
    tp2 = tp2 - dtn;    //  e+3s   e+1s   1s
    dtn = tp - tp2;     //  e+3s   e+1s   2s

    std::cout << std::boolalpha;
    std::cout << "tp == tp2: " << (tp==tp2) << std::endl;
    std::cout << "tp > tp2: " << (tp>tp2) << std::endl;
    std::cout << "dtn: " << dtn.count() << std::endl;

    return 0;
}

参考:

使用 Hugo 构建
主题 StackJimmy 设计