C++11中引入<chrono>用于处理计时相关的操作。C++14和17中对其作了一些小的改进。在即将到来的C++20中chrono即将迎来一次大范围的扩充。导致原本就比较复杂的chrono变得愈加复杂。本文局限于C++11中的chrono,做简单介绍。本文的大部分内容参考自:https://en.cppreference.com/w/cpp/chrono

C++中本没有处理计时的库,所以之前只能用C语言的ctime库。C++11中引入<chrono>,以模板的方式定义了几个计时相关的类型:

  • 表示时钟:chrono::system_clock、chrono::steady_clock、chrono::high_resolution_clock等
  • 表示时刻:chrono::time_point,时刻必须是对于某个时钟的
  • 表示时长:chrono::duration,时长可以选择

chrono::duration的模板声明如下:

template<
    class Rep, 
    class Period = std::ratio<1> 
> class duration;

其中Rep用来表示滴答的类型,用于决定其范围,Period用来表示滴答的精度(换算成秒)。由于Rep可以采用浮点类型,所以还有另外一个模板类chrono::duration_values用来返回Rep的零值,可表示的最小值/最大值等。duration的count()返回时长的滴答数。

预定义的时长类型:

std::chrono::nanoseconds	duration</*signed integer type of at least 64 bits*/, std::nano>
std::chrono::microseconds	duration</*signed integer type of at least 55 bits*/, std::micro>
std::chrono::milliseconds	duration</*signed integer type of at least 45 bits*/, std::milli>
std::chrono::seconds	duration</*signed integer type of at least 35 bits*/>
std::chrono::minutes	duration</*signed integer type of at least 29 bits*/, std::ratio<60>>
std::chrono::hours	duration</*signed integer type of at least 23 bits*/, std::ratio<3600>>

C++14还允许使用字面值来生成上面的时长:

using namespace std::literals::chrono_literals;

0.5h;
30min;
100s;
// ...

chrono::duration_cast可以在不同的时长类型之间进行转化,这其中避免不了精度的差异,需要做舍入或者进位操作。C++17中引入的floor、ceil、round等成员可以帮助设置取舍规则。

chrono::system_clock用来表示系统时钟,其成员now()可以用来获取当前系统时钟,返回值的类型是和system_clock关联的一个时刻:chrono::time_point<std::chrono::system_clock>。所谓时刻,就是某个时钟的起点到当前的时长。这里有个起点(epoch)的概念。因为计算机没办法表示无限大的数值,所以给时钟设置一个起点,才能计算它时刻。chrono::time_point的成员time_since_epoch()返回该时刻距离起点的时长。同一时钟上的时刻可以用chrono::time_point_cast来转化其时长单位。

下面是一段示例代码:

#include <ctime>
#include <chrono>
#include <iostream>
using namespace std;

int main()
{
  const auto epoch = chrono::time_point<chrono::system_clock>{};
  auto epoch_time = chrono::system_clock::to_time_t(epoch);
  cout << "epoch(ctime): " << ctime(&epoch_time);
  cout << "epoch(gmtime): " << asctime(gmtime(&epoch_time));
  cout << "duration since epoch: " << epoch.time_since_epoch().count() << endl;

  const auto today = chrono::system_clock::now();
  auto today_time = chrono::system_clock::to_time_t(today);
  cout << "today(ctime): " << ctime(&today_time);
  auto duration = today.time_since_epoch();
  cout << "today: duration since epoch: " << duration.count() << endl;
  cout << "today: duration (hours) since epoch: " << chrono::duration_cast<chrono::hours>(duration).count() << endl;
  cout << "today: duration (minutes) since epoch: " << chrono::duration_cast<chrono::minutes>(duration).count() << endl;
  return 0;
}

将上面的代码保存为showtime.cpp,然后可以用Visual Studio 2019的命令行:cl /EHsc /std:c++17 showtime.cpp编译,运行结果为:

epoch(ctime): Thu Jan  1 08:00:00 1970
epoch(gmtime): Thu Jan  1 00:00:00 1970
duration since epoch: 0
today(ctime): Wed Sep 18 23:06:16 2019
today: duration since epoch: 15688191769794584
today: duration (hours) since epoch: 435783
today: duration (minutes) since epoch: 26146986

对上面例子的一些讲解:

  • C++20之前,system_clock的epoch是可以由实现自行定义的,一般都是UTC时间1970年1月1日零点。C++20则对其进行强制规定,必须是1970年1月1日零点。从system_clock构造一个不带参数的time_point,其值就是epoch所在。
  • C++20之前,time_pont要转化为<ctime>中的time_t才能通过ctime()、gmtime()、localtime()等函数将其转化为字符串。由于ctime()转化的时候会考虑时区,所以UTC时间1970年1月1日零点对应的中国时间要加上8小时。

system_clock

system_clock的滴答的精度和范围其实是由实现自定义的,下面的例子打印出当前系统的system_clock的精度和范围:

#include <chrono>
#include <iostream>
#include <locale>
#include <typeinfo>
#include <cxxabi.h>
using namespace std;

class MyNumPunct : public std::numpunct<char>
{
protected:
  virtual char do_thousands_sep() const { return ','; }
  virtual std::string do_grouping() const { return "\03"; }
};

int main()
{
  using ScRep = chrono::system_clock::rep;
  using ScPeriod = chrono::system_clock::period;
  std::cout.imbue(locale(locale::classic(), new MyNumPunct));
  cout << "period: " << ScPeriod::num << " / " << ScPeriod::den << endl;

  auto nameOfScRep = typeid(ScRep).name();
  cout << "sep: " << nameOfScRep << ", sizeof: " << sizeof(ScRep) << endl;

  int status;
  char *demangled_name = abi::__cxa_demangle(nameOfScRep, NULL, NULL, &status);
  if(status == 0) {
    cout << "sep (demangled): " << demangled_name << endl;
    free(demangled_name);
  }
  return 0; 
}

上面例子中的代码参考了:

其中使用到了GCC的abi,所以需要使用GCC来编译。

在Windows 10下输出:

period: 1 / 1,000,000,000
sep: x, sizeof: 8
sep (demangled): long long

duration_cast

不同的duration,由于其嘀嗒时长以及范围不同,转化的时候会有精度损失的情况存在,这种情况下需要显示调用duration_cast来转化。

这里有几个规则:

  • 由低精度往高精度转的时候(比如从小时转为分钟),因为不会出现精度损失,所以不需要duration_cast
  • 反而言之,由高精度往低精度转的时候(比如从分钟到小时),因为会出现精度损失,需要duration_cast
  • 如果duration的范围是用整形表示的,那么转化为浮点型的时候不需要duration_cast,会有相应的构造函数进行处理,这里隐含的意义是,浮点数的表示范围一定比整型值大。可以用chrono::treat_as_floating_point来检查duration的范围是否是浮点树类型。
  • 从浮点型的往整型值转的时候,不仅有可能出现精度损失,甚至可能出现未定义行为,因为浮点数的值有可能是NaN。

另外,C++17中还引入了floor, ceil, round, abs等还处理转化过程中的精度问题。

(完)