std::chrono
一、概述
时间库的基本组成部分包括duration(时间段)、time_point(时间点)和clock(时钟)。
时间库所在的头文件为:
#include <chrono>
时间库的各种类型都定义在std::chrono
命名空间中。
二、duration
时长类
duration
用来表示一个时间间隔,即两个时间点之间的时间差。如两个小时、三天等,都属于时长。
2.1 初始化
C++通过计时单元个数和计时单元确定时长,即:
时长 = 计时单元个数 × 计时单元 时长=计时单元个数\times计时单元 时长=计时单元个数×计时单元
- 一个计时单元定义为1s的若干倍,具体的倍数可以自行指定
- 如果定义计时单元个数为20,计时单元为1s的 1 4 \frac{1}{4} 41倍,则表示的时长就是 20 × 1 4 × 1 s = 5 s 20\times\frac{1}{4}\times1s=5s 20×41×1s=5s
因此duration
类定义为有两个参数的模板:
template<
class Rep,
class Period = std::ratio<1>
> class duration;
Rep
:一个数值类型。指定计时单元个数的类型,创建duration
对象时需要传入一个Rep
类型的数字表示计时单元的个数r
。可以使用成员函数d.count()
获取duration
对象中计时单元的个数r
Period
:一个数值类型。用来确定一个计时单元的大小为 该数值类型对象表示的值 × 1 s 该数值类型对象表示的值\times1s 该数值类型对象表示的值×1s(1秒的多少倍)。std::ratio
是一个表示有理数的模板数值类型,根据传入的两个值模板参数,构成了一个数 N u m D e n o m \frac{Num}{Denom} DenomNum的类型
为了简化部分数的表示,C++预先定义了表示template< std::intmax_t Num, std::intmax_t Denom = 1 > class ratio; std::ratio<1, 2> half; // half 表示 1/2 std::ratio<7, -21> neg_one_three // 表示 -1/3 的类型
0.001
、0.01
、0.1
、10
、100
等数字的类型:using std::milli = std::ratio<1, 1000> // 表示 0.001 的类型 using std::kilo = std::ratio<1000, 1> // 表示 1000 的类型 ```
Period
如果是std::ratio<1, 2>
,表示一个数 1 2 \frac{1}{2} 21的类型,该类型的对象表示数字 1 2 \frac{1}{2} 21,那么根据Period
可以确定该duration
中一个计时单元就是 1 2 × 1 s \frac{1}{2}\times1s 21×1s,即计时单元为0.5s。
duration
对象表示的时长为计时单元个数×计时单元,即: r × N u m D e n o m × 1 s r\times\frac{Num}{Denom}\times1s r×DenomNum×1s。如:// Rep 为 double 类型,r 为 2.5 // Period 为 std::ratio<1, 5>,Num 为 1,Denom 为 5,表示一个分数 1/5 // 所以 d1 就表示 2.5 × 1 / 5 × 1s = 0.5s 的时长 std::chrono::duration<double, std::ratio<1, 5>> d1(2.5) std::chrono::duration<int, std::milli> second(1000); // 表示 1秒
为了简化时长的表示,C++预先定义了特例化的毫秒、秒、分钟等类型:
// 毫秒
using std::chrono::milliseconds = std::chrono::duration</* int45 */, std::milli>;
// 秒
using std::chrono::seconds = std::chrono::duration</* int35 */>;
// 表示 1000 毫秒的 duration 对象
std::chrono::milliseconds ms(1000);
// 表示 10 秒的 duration
std::chrono::seconds s(10);
2.2 时长字面值后缀
在命名空间 std::literals::chrono_literals
中定义了一系列字面值后缀运算符h
、min
、s
、ms
等,用于简化时长的表示:
std::chrono::seconds operator""s(n)
:返回一个表示n
秒的duration
对象std::chrono::milliseconds operator""ms(n)
:返回一个表示n
毫秒的duration
对象
对于以上重载的字面值后缀运算符,可以传入整数或浮点数,编译器会根据需要自动选择合适的duration
中Rep
的类型,因此如果对精度有特殊要求,不应该使用这种方式。
auto two_seconds = 2s; // 等价于 std::chrono::seconds(2)
std::chrono::seconds four_seconds(4s); // 等价于调用 std::duration 的拷贝构造
2.3 duration
之间隐式类型转换
std::duration
的不同类型之间可以相互隐式类型转换,由于每种类型duration
的计时单元是固定的,因此不同类型的转换就是将n
个以p1
为计时单元的duration
转换为m
个以p2
为计时单元的duration
。实际就是计算n
在新的计时单元下对应的m
值为多少。
如:
std::chrono::duration<int, std::ratio<3, 1>> int_d(6)
表示n=6
个以3s为单位的duration
,共18s的时长std::chrono::duration<double, std::ratio<2, 1>> dou_d(int_d)
表示将int_d
隐式转换为以2s为单位的duration
,记dou_d
的count()
返回的计时单位的个数为m
此时double类型m
的计算过程为:
6
∗
3
1
∗
1
s
=
m
∗
2
1
∗
1
s
m
=
6
∗
3
2
=
9.0
\begin{aligned} 6*\frac{3}{1}*1s &= m*\frac{2}{1}*1s \\ m &= \frac{6*3}{2} = 9.0 \end{aligned}
6∗13∗1sm=m∗12∗1s=26∗3=9.0
即计算方法为:
m
=
n
∗
p
1
p
2
m=\frac{n*p1}{p2}
m=p2n∗p1
为了能够正确执行转换,需要保证转换时精度不会损失,否则编译器会报错,为了判断精度是否会损失,有以下方法:
m
是浮点数类型时才能正常转换。因为 n ∗ p 1 p 2 \frac{n*p1}{p2} p2n∗p1的结果可能是小数,如果m
不是浮点数类型,结果转换为m
时浮点数的精度会丢失。m
如果是非浮点数类型,需要保证 p 1 p 2 \frac{p1}{p2} p2p1为整数,这样才能保证m
的结果也是整数,不会出现从浮点数类型的结果转换为m
时造成精度的丢失
2.4 duration
之间显式类型转换
如果确实需要将高精度的类型转换为低精度的类型,可以使用std::chrono::duration_cast
执行显式类型转换,但是这可能会造成精度的丢失:
// int_three_two 表示 12s 的时长
std::chrono::duration<int, std::ratio<3, 2>> int_three_two(8);
// int_seven_five count 的计算过程为 12 / (7 / 5) = 12 * 5 / 7 = 60 / 7 = 8.571...
std::chrono::duration<int, std::ratio<7, 5>> int_seven_five =
std::chrono::duration_cast<std::chrono::duration<int, std::ratio<7, 5>>>(
int_three_two);
// 由于目标的 Rep 为 int,所以转换时会丢失小数,最终输出 8
// 所以 int_seven_five 实际表示的是 8 * 7 / 5 * 1s = 11.2s 的时长
std::cout << int_seven_five.count() << std::endl;
2.5 duration
对象的运算
duration
类重载了多种运算符,支持多种操作:
- 与对应
Rep
类型的数字n
的加减乘除,表示对其计数单元的个数进行对应运算 - 两个
duration
类型相加减,表示计算时长累加或相减后的新时长
2.6 输出duration
对象
在C++20中,duration
对象还重载了输出运算符operator<<
,该运算符会输出对象的cout()
,并根据Period
类型的不同添加后缀字符,详细的规则可以查看文档。
std::chrono::seconds cout_second(2);
std::cout << "cout_second: " << cout_second << std::endl;
// cout_second: 2s
std::chrono::milliseconds cout_mill_second(300);
std::cout << "cout_mill_second: " << cout_mill_second << std::endl;
// cout_mill_second: 300ms
std::chrono::duration<double, std::ratio<2, 1>> cout_duration(2.88);
std::cout << "cout_duration: " << cout_duration << std::endl;
// cout_duration: 2.88[2]s
三、时钟类
时钟类主要用来获取当前的时间,常见的时钟类包括:
std::chrono::system_clock
:该时钟的时间对应于操作系统提供的时间,如果修改了操作系统的时间,那么利用该类获取的时间也会随之变化(第二次获取的时间可能会早于第一次获取的时间)std::chrono::steady_clock
:该时钟是稳定的,即时间一定只会向前走,不会发生第二次时间比第一次早的情况std::chrono::high_resolution_clock
:利用上面的某种时钟定义的高精度时钟,可以提供能够实现的最短计时单元的时钟
时钟类内部定义了一个std::chrono::duration
的特例化类型,用来表示时钟的计时单元,一般由编译器确定具体的数值,如在MSVC某版本中定义为了duration<long long, ratio<1, 10'000'000>
。因此,对于high_resolution_clock
,其计时单元应该比其余两种更短,以提供更高的精度。
时钟类中都包含一个静态成员函数now
,且其中定义了一个std::chrono::time_point
的特例化类型,作为now
的返回值类型:
// std::chrono::system_clock 返回的 time_point 第一个模板参数为 std::chrono::system_clock
std::chrono::time_point<std::chrono::system_clock> now_time
= std::chrono::system_clock::now();
// std::chrono::steady_clock 返回的 time_point 第一个模板参数为 std::chrono::steady_clock
std::chrono::time_point<std::chrono::steady_clock> now_time
= std::chrono::steady_clock::now();
四、时间点
4.1 ctime
中时间的表示
C++的#include <ctime>
头文件中保留了C语言中时间相关的函数。
函数time
用来获取当前的时间:
std::time_t time(nullptr);
- 返回值是一个整数类型
std::time_t
(表示1970年到当前时间的秒数)
可以使用std::ctime
函数将std::time_t
类型表示的时间转换为字符串形式:
std::time_t cur_time = std::time(nullptr);
std::cout << "time_t: " << cur_time << std::endl;
// time_t: 1722080852
// ctime 将 time_t 类型的时间转换为字符串
char *cur_time_str = std::ctime(&cur_time);
std::cout << "ctime: " << cur_time_str << std::endl;
// ctime: Sat Jul 27 19:47:32 2024
为了更好的表示和处理时间,还定义了一个std::tm
类型的结构体,其中包含了表示年月日,时分秒等数据成员。可以使用如下两个方法,将time_t
类型表示的时间转换为std::tm
类型:
std::tm* localtime(const std::time_t* time);
:转换为std::tm
类型相对于GMT偏移后的本地时std::tm* gmtime(const std::time_t* time);
:转换为std::tm
类型的GMT标准世界时
如果想以字符串输出std::tm
类型的对象,需要使用std::asctime
函数:
char* asctime(const std::tm* time_ptr);
如果想自定义std::tm
类型的对象输出字符串的格式,需要使用函数:
std::size_t strftime( char* str, // 格式化后字符串保存的位置
std::size_t count, // str 可以写入的最大字符个数
const char* format, // 指定输出字符串的格式
const std::tm* tp ); // 要转换的 std::tm 对象
下面展示了以上方法的使用效果,包含了:
- 将
std::time_t
类型转换为std::tm
类型 - 使用
std::asctime
将std::tm
类型转换为固定的字符串格式 - 使用
std::strftime
将std::tm
类型转换为指定的格式
// 转换为本地时间
std::tm *local_tm = std::localtime(&cur_time);
// 将 std::tm 转换为字符串
char *local_tm_str = std::asctime(local_tm);
std::cout << "local_time: " << local_tm_str << std::endl;
// local_time: Sat Jul 27 19:47:32 2024
// 如果使用 gmtime 函数,得到的结果为
// gmt_time: Sat Jul 27 11:47:32 2024
// 自定义 std::tm 输出的字符串格式
char cus_local_time_str[100];
std::strftime(cus_local_time_str, sizeof(cus_local_time_str),
"%Y-%m-%d %H:%M:%S", local_tm);
std::cout << "cus_local_time_str: " << cus_local_time_str << std::endl;
// cus_local_time_str: 2024-07-27 19:47:32
// 如果使用 gmtime 函数,得到的结果为
// cus_gmt_time_str: 2024-07-27 11:47:32
4.2 time_point
类型
表示一个时间点,是一个包含两个模板参数的类,定义如下:
template<
class Clock,
class Duration = typename Clock::duration
> class time_point;
time_point
类似于time_t
,利用std::duration
类型的对象保存从1970年开始,到达所表示时间点的时长。因此,模板参数表示的意义为:
class Clock
:表示时间点内相关数据的表示参考哪个时钟类class Duration
:表示时间点内数据保存的计时单元。从定义可以看出,默认使用的是Clock
参数内的duration
定义
time_point
类型包含如下构造函数:
time_point();
:默认构造,表示1970年那个参考时间的时间点time_point(const duration& d);
:表示从1970年的参考时间开始,经过时间d
后的时间点time_point(time_point <Clock, Duration2>& t);
:将t
表示转换为duration
对象后,构造一个时间点。使用这种方法要保证Duration2
可以隐式转换为当前time_point
内的duration
的类型
通常,获取time_point
最简单的方法就是使用时钟类的静态成员函数now
。
// def_tp 表示 1970年1月1日0点
std::chrono::time_point<std::chrono::system_clock> def_tp();
// cus_tp 表示 1970年1月1日0点经过 2s 后的时间
std::chrono::time_point<std::chrono::system_clock> cus_tp(std::chrono::seconds(2));
// now_tp 表示当前时间
auto now_tp = std::chrono::system_clock::now();
time_point
常用的一个成员函数为:
duration time_since_epoch() const;
- 返回
duration
表示的从1970年到time_point
时间点的时长
示例:
// 返回时间点到 1970 年的时长
auto cus_tp_duration_to_epoch = cus_tp.time_since_epoch();
// 将时长转换为 ms
std::chrono::milliseconds cus_tp_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(cus_tp_duration_to_epoch);
std::cout << cus_tp_ms.count() << std::endl; // 2000
time_point
有很多重载的运算符,以实现对时间点的前后移动:
- 自增
++
/自减--
:对内部存储从1970到所表示时间点的duration的计时单元个数+/-1 - 加减一个
std::chrono::duration
:返回相对于time_point
所表示时间偏移duration
后的新时间点 - 两个
time_point
相减:返回duration
所表示两个时间点之间的时长 - 两个
time_point
做比较:通过time_since_epoch
成员函数返回的结果做比较
注意:
- 以上重载的运算符如果涉及到两个
time_point
,必须保证两个time_point
所关联的时钟类相同,即它们的第一个模板参数类型是同一个Clock
。 - 以上重载的运算符,如果涉及到
duration
作为操作数或返回值:- 如果是作为操作数,不要求
duration
的计时单元和time_point
内部duration
的计时单元相同 - 如果返回一个
duration
的值,会根据duration
的计时单元和time_point
内部duration
的计时单元的公共类型作为返回值duration
的类型(即两者都可以隐式转换到的duration
类型)
- 如果是作为操作数,不要求
示例:
// 自定义时间点内部的 duration 长度
std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>
tp_druation_second(std::chrono::seconds(5));
// 对内部的 duration 的计时单元数量 +1
++tp_druation_second;
std::cout << tp_druation_second.time_since_epoch() << std::endl; // 6s
// 测试时间点的移动
tp_druation_second += std::chrono::seconds(2);
std::cout << tp_druation_second.time_since_epoch() << std::endl; // 8s
// 创建新的时间点,返回新的时间点
// 递增 9.6s
auto tp_duration_second_new = tp_druation_second +
std::chrono::duration<double, std::ratio<3, 1>>(3.2);
// 根据 C++ 20 下的输出,可以推测结果类型为 std::chronon::duration<double, std::chrono::seconds>
std::cout << tp_duration_second_new.time_since_epoch() << std::endl; // 17.6s
// 两个时间点相减,返回 duration
auto tp_duration_second_diff = tp_duration_second_new - tp_druation_second;
std::cout << tp_duration_second_diff << std::endl; // 9.6s
4.3 time_point
转换ctime
只有std::chrono::system_clock
提供了两个静态成员函数,用于std::chrono::system_clock::time_point
和std::time_t
之间的类型转换:
std::time_t to_time_t(const time_point& t)
std::chrono::system_clock::time_point from_time_t( std::time_t t )
示例:
auto now_tp = std::chrono::system_clock::now();
// 将时间点转换为 time_t
std::time_t now_tp_time_t = std::chrono::system_clock::to_time_t(now_tp);
std::cout << std::ctime(&now_tp_time_t) << std::endl;
// Sun Jul 28 09:27:00 2024