一文读懂C++ chrono库: duration, clocks, date, timezone

C++ 的 <chrono> 库提供了一套功能强大且类型安全的时间与日期管理工具. 从 C++11 引入的 std::chrono, 到 C++20 的增强功能, 开发者可以高效地处理时间间隔, 时间点, 日期和时区. 本篇博客将通过实例讲解这些功能.

分数(std::ratio)

C++中的std::ratio是一个模板类, 定义在<ratio>头文件中, 用于编译期处理有理数(rational number). 它主要用于以高效且类型安全的方式表示和操作以分数形式表示的常量值.

下面是std::ratio的模板定义

template<std::intmax_t Num, std::intmax_t Den = 1>
struct ratio;

定义分数

定义一个分数:

using r1 = std::ratio<1, 60>;  // 代表 1/60
using r2 = std::ratio<1, 2>;   // 代表 1/2

获取分子/分母:

std::cout << r1::num << "/" << r1::den << "\n"; // 输出: 1/60

注意这些数值是编译时常量, 不能用普通变量:

int a = 1, b = 3;
using r3 = std::ratio<a, b>; // Error

运算和比较

标准库提供了一些类型别名和函数支持std::ratio的比较和运算.

  1. 算数运算: ratio_add, ratio_subtract, ratio_multiply, ratio_divide
using r3 = std::ratio<1, 2>;
using r4 = std::ratio<1, 3>;

using r5 = std::ratio_add<r3, r4>;       // 5/6
using r6 = std::ratio_subtract<r3, r4>;  // 1/6
using r7 = std::ratio_multiply<r3, r4>;  // 1/6
using r8 = std::ratio_divide<r3, r4>;    // 3/2
  1. 比较运算: ratio_equal, ratio_not_equal, ratio_less, ratio_less_equal, ratio_greater, ratio_greater_equal
using r9 = std::ratio<1, 3>;
using r10 = std::ratio<2, 6>;
std::cout << std::boolalpha << std::ratio_equal<r9, r10>::value
        << std::endl;  // true

预定义的别名

C++标准库提供了一些常用的比例类型别名:

using atto = std::ratio<1, 1'000'000'000'000'000'000>;
using femto = std::ratio<1, 1'000'000'000'000'000>;
using pico = std::ratio<1, 1'000'000'000'000>;
using nano = std::ratio<1, 1'000'000'000>;
using micro = std::ratio<1, 1'000'000>;
using milli = std::ratio<1, 1'000>;
using centi = std::ratio<1, 100>;
using deci = std::ratio<1, 10>;
using deca = std::ratio<10, 1>;
using hecto = std::ratio<100, 1>;
using kilo = std::ratio<1'000, 1>;
using mega = std::ratio<1'000'000, 1>;
using giga = std::ratio<1'000'000'000, 1>;
using tera = std::ratio<1'000'000'000'000, 1>;
using peta = std::ratio<1'000'000'000'000'000, 1>;
using exa = std::ratio<1'000'000'000'000'000'000, 1>;

这些别名常与std::chrono配合使用以表示时间单位.


二. 持续时间 (Durations)

std::duration 是 C++11 引入的一个模板类, 定义在头文件 <chrono> 中, 用于表示和处理时间间隔(持续时间). 它以一个 时间单位数量值 的组合形式, 表示经过的时间长度, 支持灵活的时间单位和类型安全的运算.


定义std::duration

模板定义

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

比如, 定义一秒可以用如下方式:

std::chrono::duration<long, std::ratio<1, 1>> d1;
std::chrono::duration<long, std::ratio<1>> d2;
std::chrono::duration<long> d3;

定义一分钟

std::chrono::duration<long, std::ratio<60>> d4;

支持的操作

算数运算

std::chrono::duration<long, std::ratio<1>> d5{14};  // = 14 seconds
++d5;                                               // = 15 seconds
d5 *= 2;                                            // = 30 seconds

比较运算

std::chrono::duration<long, std::ratio<60>> d6{10};  // = 10 minutes
if (d5 > d6) {
  std::cout << "d5 > d6" << std::endl;
} else {
  std::cout << "d5 <= d6" << std::endl;
}

两个时间段相加:

// 结果以分钟作为单位
std::chrono::duration<double, std::ratio<60>> d7{d5 + d6};

// 结果以秒作为单位
std::chrono::duration<long, std::ratio<1>> d8{d5 + d6};

std::cout << std::format("{}min + {}sec = {}min or {}sec", d6.count(),
                          d5.count(), d7.count(),
                          d8.count())
          << std::endl;  // 10min + 30sec = 10.5min or 630sec

时间单位转换

std::chrono 提供了显式的时间单位转换支持:

std::chrono::duration<long> d9{30};                     // = 30 seconds
std::chrono::duration<double, std::ratio<60>> d10{d9};  // = 0.5 minutes
std::cout << std::format("{}sec = {}min", d9.count(),
                          d10.count());  // 30sec = 0.5min

如果转换的时候不安全, 则会编译失败:

// long类型无法表示0.5, 会导致编译失败
std::chrono::duration<long, std::ratio<60>> d11{d9}; // Error!

如果强转回丢失精度

// 强转 (0 instead of 0.5)
auto d12{duration_cast<std::chrono::duration<long, std::ratio<60>>>(d9)};  // = 0 minutes

常见别名和单位

别名表示Period (std::ratio)
std::chrono::hours小时std::ratio<3600, 1>
std::chrono::minutes分钟std::ratio<60, 1>
std::chrono::secondsstd::ratio<1, 1>
std::chrono::milliseconds毫秒std::ratio<1, 1000>
std::chrono::microseconds微秒std::ratio<1, 1000000>
std::chrono::nanoseconds纳秒std::ratio<1, 1000000000>

时间字面量

预定义的一些时间字面量: h, min, s, ms, us, ns

使用如下的using语句就可以:

using namespace std;
using namespace std::literals;
using namespace std::chrono_literals;
using namespace std::literals::chrono_literals;

定义一分钟:

// 使用预定义的单位
std::chrono::minutes d12{1}; // = 1 minute
// 使用字面量
using namespace std::literals::chrono_literals;
auto d13{1min}; // = 1 minute
std::cout << std::boolalpha << (d12 == d13) << std::endl;  // true

auto d14{std::chrono::hours{1} + std::chrono::minutes{23} +
          std::chrono::seconds{45}};     // 5025 seconds
std::cout << d14.count() << std::endl;  // 5025

三. 时钟 (Clocks)

C++ 中的 clocks(时钟)是用于获取当前时间点或测量时间的工具, 定义在头文件 <chrono> 中. C++ 提供了多种类型的时钟, 每种都适用于不同的场景, 例如系统时间, 高精度计时和稳定时间测量等.


时钟类型

std::chrono 提供以下时钟:

每个时钟都提供now()方法, 用来获取当前时间.

时钟的基本操作

获取当前时间点

std::chrono::system_clock::time_point tpoint{std::chrono::system_clock::now()};

或者

auto tpoint{std::chrono::system_clock::now()};

输出时间

// Convert to a time_t.
time_t tt{std::chrono::system_clock::to_time_t(tpoint)};
// Convert to local time.
tm* t{localtime(&tt)};
// Write the time to the console.
std::cout << std::put_time(t, "%H:%M:%S\n"); // 17:25:54

获取运行耗时

#include <chrono>
#include <cmath>
#include <format>
#include <iostream>
#include <ratio>

int main() {
  // 获取开始时间
  auto start{std::chrono::high_resolution_clock::now()};
  // 模拟一些工作
  double d{0};
  for (int i{0}; i < 1'000'000; ++i) {
    d += sqrt(sin(i));
  }
  // 获取结束时间
  auto end{std::chrono::high_resolution_clock::now()};

  // 计算时间差
  auto diff{end - start};
  // 转为毫秒
  std::cout << std::chrono::duration<double, std::milli>{diff}.count()
            << "ms\n";
  return 0;
}

四. 时间点 (Time Points)

在 C++ 的 <chrono> 库中, 时间点(Time Point) 是指在某个时钟(Clock)上的特定时刻. std::chrono::time_point 是一个模板类, 用于表示某个具体的时间.


定义

template <class Clock, class Duration = typename Clock::duration>
class time_point;

特点:

常用操作

创建时间点

auto now = std::chrono::system_clock::now();

时间点加减时间段:

auto future = now + std::chrono::hours(1); // 当前时间点加一小时
auto past = now - std::chrono::minutes(30); // 当前时间点减30分钟

计算两个时间点的差:

auto duration = future - now;  // 得到时间间隔
std::cout
    << std::chrono::duration_cast<std::chrono::minutes>(duration).count()
    << " minutes\n";  // 60 minutes

转换为可读时间

auto now_c = std::chrono::system_clock::to_time_t(now);
std::cout << "Current time: "
          << std::ctime(&now_c);  // Current time: Wed Jan 22 18:12:17 2025

时间单位转换

using namespace std::chrono;
time_point<steady_clock, seconds> tp_second{42s};
// 从秒转为毫秒
time_point<steady_clock, milliseconds> tp_milliseconds{tp_second};
std::cout << tp_milliseconds.time_since_epoch().count() << "ms\n";  // 42000ms

五. 日期 (Dates)

C++ 在 C++20 中引入了 <chrono> 的扩展部分, 用于更高效和类型安全地处理日期(Dates). 这些新功能提供了一套现代化的日期和时间管理接口, 适用于日期的存储, 操作和格式化.

核心概念

std::chrono命名空间里面定义了: year, month, day, weekday, weekday_indexed, weekday_last, month_day, year_month, year_month_day等日期格式.

常用操作

定义日期

using namespace std::chrono;

year y{2025};
month m{1};
day d{21};
year_month_day date{y, m, d};
std::cout << "Date: " << date << "\n";  // 输出 2025-01-21

也可以用下面的方式创建

auto date1 = {y / m / d};
auto date2 = {d / m / y};
year_month_day date3{Wednesday[4] / January / 2025};

日期加减

auto next_month = date + months{1};  // 加一个月

格式化日期

std::cout << std::format("{:%Y-%m-%d}\n", date);

sys_days

表示从 1970-01-01(UTC 的 Unix 时间原点)起的天数, 便于日期的算术操作. 支持的操作有:

  1. 获取sys_days

    auto today{floor<days>(system_clock::now())};
  2. sys_days转为time_point

    system_clock::time_point t1{today};
  3. time_point转为sys_days

    year_month_day date4{floor<days>(t1)};

六. 时区 (Time Zones)

时区数据库(Timezone database): 获取时区列表.

可以使用std::chrono::get_tzdb()获取到数据库.

using namespace std::chrono;
const auto& database{get_tzdb()};
for (const auto& timezone : database.zones) {
  std::cout << timezone.name() << std::endl;
}

获取特定的时区:

auto* hk{locate_zone("Asia/Hong_Kong")};
auto* gmt{locate_zone("GMT")};
auto* current{current_zone()};

将当前时间转为 GMT 时区时间

auto now = system_clock::now();
auto gm_now = gmt->to_local(now);
std::cout << gm_now << std::endl;

或者使用 zoned_time

zoned_time zt{hk, now};
std::cout << "Current time in HongKong: " << zt << '\n';

总结

C++ 的时间和日期库功能强大, 从编译期的比例计算到运行时的时区支持, 都提供了高效, 安全的解决方案. 通过善用这些工具, 可以大大简化时间相关的复杂操作, 并提升代码的可读性与可维护性.

源码链接

源码链接