1. 引言:为什么需要 std::chrono
在 C++11 之前,C++ 程序处理时间几乎只能依赖 C 标准库的 time_t / struct tm 与一堆全局函数(time()、localtime()、strftime()),其痛点非常明显:
| 痛点 | C 风格时间 API | std::chrono 的解法 |
|---|---|---|
| 精度粗糙 | time_t 通常只有秒级 | 模板化 duration,从纳秒到年的任意单位 |
| 单位混乱 | 秒、毫秒、微秒全靠魔法数字 | 编译期强类型单位,1s、500ms 一目了然 |
| 时钟不区分 | 只有一个"系统时间",无法判断是否单调 | 时钟类型系统:system_clock / steady_clock 等 |
| 算术易错 | time_t 相减得到秒数,语义弱 | time_point 相减得到 duration,类型安全 |
| 时区/日历缺失 | 全靠 localtime 等线程不安全函数 | C++20 日历类型 + IANA 时区数据库 |
| 可读性差 | 裸整数,无法表达"3 天 5 小时" | std::chrono::days{3} + std::chrono::hours{5} |
std::chrono 的设计哲学可以概括为一句话:把时间的所有维度(时刻、间隔、单位、时钟、日历、时区)都变成强类型,把错误从运行时提前到编译期。它不是简单地封装几个系统调用,而是一套完整的时间代数系统。
2. 核心三件套:duration / time_point / clock
std::chrono 的整个体系建立在三个相互关联的抽象之上:
┌─────────────────────────────────────────────────────────┐
│ duration<Rep, Period> —— 时间间隔("多长时间") │
│ ▲ │
│ │ time_point = clock::epoch + duration │
│ time_point<Clock, Duration> —— 时刻("什么时候") │
│ ▲ │
│ │ 时钟提供 epoch 与当前时刻 │
│ Clock(system/steady/utc/tai/...)—— 时间源 │
└─────────────────────────────────────────────────────────┘
2.1 duration:时间间隔
cpp
namespace std::chrono {
template<class Rep, class Period = std::ratio<1>>
class duration;
}
- Rep:计数类型,通常是整数(long long)或浮点数(double)。
- Period:一个 std::ratio<Num, Den>,表示每个 tick 相当于多少秒。ratio<1> 即 1 秒。
预定义类型(C++20 起全部有对应别名与字面量):
| 类型别名 | Period | 字面量 | 一秒包含的 tick 数 |
|---|---|---|---|
| std::chrono::nanoseconds | ratio<1, 1'000'000'000> | ns | 1e9 |
| std::chrono::microseconds | ratio<1, 1'000'000> | us | 1e6 |
| std::chrono::milliseconds | ratio<1, 1'000> | ms | 1e3 |
| std::chrono::seconds | ratio<1> | s | 1 |
| std::chrono::minutes | ratio<60> | min | 1/60 |
| std::chrono::hours | ratio<3600> | h | 1/3600 |
| std::chrono::days(C++20) | ratio<86400> | d | 1/86400 |
| std::chrono::weeks(C++20) | ratio<604800> | --- | 1/604800 |
| std::chrono::months(C++20) | ratio<2629746>(平均月) | --- | --- |
| std::chrono::years(C++20) | ratio<31556952>(平均年) | y | --- |
注意 months 和 years 的 Period 是平均值(2629746 秒 = 365.2425 天 / 12),因为它们不是固定长度。日历运算中 months/years 与 days 的换算必须走 duration_cast(截断),这正是 C++20 把日历操作设计成独立类型的原因之一。
2.2 time_point:时刻
cpp
namespace std::chrono {
template<class Clock, class Duration = typename Clock::duration>
class time_point;
}
time_point 是相对于某个时钟 epoch 的偏移量,本质上是 duration 的薄封装:
cpp
template<class Clock, class Duration>
class time_point {
Duration d_; // 距 epoch 的偏移
public:
constexpr Duration time_since_epoch() const noexcept;
// +、-、比较运算由 duration 驱动
};
核心代数规则(类型系统保证了语义正确性):
| 表达式 | 结果类型 | 语义 |
|---|---|---|
| tp2 - tp1 | duration | 两个时刻的间隔 |
| tp + d / tp - d | time_point | 时刻偏移 |
| tp1 < tp2 | bool | 时刻先后比较 |
| tp.time_since_epoch() | duration | 距 epoch 的偏移 |
time_point 要求 Clock 与 Duration 完全匹配才能相减------两个不同时钟的 time_point 直接相减是编译错误,这杜绝了"拿单调时钟的时刻去减墙上时钟的时刻"这类荒谬操作。
2.3 Clock:时间源
每个时钟都是一个类,提供三个核心成员:
cpp
struct some_clock {
using rep = /* 计数类型 */;
using period = /* tick 周期 */;
using duration = duration<rep, period>;
using time_point = std::chrono::time_point<some_clock>;
static constexpr bool is_steady = /* 是否单调 */;
static time_point now() noexcept;
};
C++11 提供三个时钟,C++20 扩展为完整的时钟家族:
| 时钟 | 语义 | is_steady | 可转 time_t | 底层来源 |
|---|---|---|---|---|
| system_clock | 系统墙上时钟(UTC) | 否 | 是 | Windows: GetSystemTimeAsFileTime;Linux: clock_gettime(CLOCK_REALTIME) |
| steady_clock | 单调时钟,只增不减 | 是 | 否 | Windows: QueryPerformanceCounter;Linux: clock_gettime(CLOCK_MONOTONIC) |
| high_resolution_clock | 实现定义的"最高精度"时钟 | 取决于实现 | 取决于实现 | 通常是前两者的别名 |
| utc_clock(C++20) | UTC 时间,含闰秒 | 否 | 是 | 系统时钟 + 闰秒表 |
| tai_clock(C++20) | 国际原子时 | 否 | 否 | 基于 utc 偏移 |
| gps_clock(C++20) | GPS 时间 | 否 | 否 | 基于 utc 偏移 |
| file_clock(C++20) | 文件系统时间戳时钟 | 否 | 否 | Windows FILETIME / POSIX 时间 |
high_resolution_clock 是最容易踩坑的时钟:标准不保证它单调 ,在 MSVC 中它通常是 steady_clock 的别名,在 libstdc++ 中历史上曾是 system_clock 的别名。测量代码耗时永远应该直接用 steady_clock,而不是 high_resolution_clock。
3. duration 的编译期单位换算与 ratio
std::ratio 是 chrono 的基石,它提供编译期有理数算术:
cpp
template<intmax_t Num, intmax_t Den = 1>
class ratio {
public:
static constexpr intmax_t num = /* 约分后的分子(Den>0 时非负) */;
static constexpr intmax_t den = /* 约分后的分母(恒正) */;
using type = ratio<num, den>;
};
3.1 ratio 的编译期化简
ratio<6, 8> 会被自动约分为 ratio<3, 4>。其内部通过 std::__static_gcd(libstdc++)/ std::gcd 的编译期版本计算最大公约数,并保证:
- Den 恒为正(若传入负数,符号归一化到分子);
- num 与 den 互质。
3.2 ratio 算术
标准库提供五个编译期运算:
| 运算 | 结果 | 示例 |
|---|---|---|
| ratio_add<R1, R2> | R1 + R2 | ratio_add<ratio<1,3>, ratio<1,6>> = ratio<1,2> |
| ratio_subtract<R1, R2> | R1 - R2 | --- |
| ratio_multiply<R1, R2> | R1 * R2 | --- |
| ratio_divide<R1, R2> | R1 / R2 | --- |
| ratio_equal<R1, R2> | 是否相等 | --- |
这些运算全部在编译期完成,不会产生任何运行时开销。
3.3 duration 的编译期换算原理
duration 之间的转换,其核心是 common_type 特化与 duration_cast 中的编译期公式。当从 duration<Rep1, Period1> 转换为 duration<Rep2, Period2> 时,转换因子是:
factor = Period1 / Period2 (编译期有理数)
count2 = count1 * factor.num / factor.den
例如 hours(1) 转 seconds:Period1 = ratio<3600>,Period2 = ratio<1>,factor = 3600/1,所以 1 * 3600 = 3600 秒。整个过程在编译期确定缩放系数,运行时只有一个乘除法。
3.4 为什么隐式转换有条件
duration 的隐式转换(构造函数非 explicit)有严格条件(time.duration.cons):
当且仅当 treat_as_floating_point<Rep2> 为 true,或 ratio_divide<Period1, Period2>::den == 1 时,duration<Rep1, Period1> 可隐式转换为 duration<Rep2, Period2>。
含义:
- 目标 Rep 是浮点 → 任何精度都能无损表示 → 允许隐式;
- 否则要求 Period1 / Period2 是整数(即 Period2 能整除 Period1),转换不丢 tick。
因此:
cpp
auto s = std::chrono::seconds(90);
std::chrono::minutes m = s; // 编译错误!90 秒不能无损表示成整数分钟
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(s); // 显式转换 OK
std::chrono::duration<double> ds = s; // 隐式 OK,浮点 Rep
这个设计把"可能丢精度"的转换强制为显式,把"必然无损"的转换保留为隐式,是类型系统在时间领域的经典应用。
4. duration 转换、截断与四舍五入
4.1 duration_cast:向零截断
cpp
template<class ToDuration, class Rep, class Period>
constexpr ToDuration duration_cast(const duration<Rep, Period>& d);
C++11 的 duration_cast 采用向零截断(truncation toward zero),即 +2.7 → 2、-2.7 → -2。它适合单位换算,但不适合"取整到某精度"的场景(负值行为与直觉不符)。
4.2 C++17 的三兄弟:floor / ceil / round
cpp
template<class ToDuration, class Rep, class Period>
constexpr ToDuration floor(const duration<Rep, Period>& d); // 向下取整
constexpr ToDuration ceil (const duration<Rep, Period>& d); // 向上取整
constexpr ToDuration round(const duration<Rep, Period>& d); // 四舍五入(远离零)
| 输入 | duration_cast | floor | ceil | round |
|---|---|---|---|---|
| 3.7s → s | 3 | 3 | 4 | 4 |
| -3.7s → s | -3 | -4 | -3 | -4 |
| -3.2s → s | -3 | -4 | -3 | -3 |
round 对 x.5 采用远离零的规则(2.5 → 3、-2.5 → -3),与 std::round 一致。
4.3 内部实现原理
duration_cast 的实现核心是一个编译期分派:当转换因子可整除时直接用乘法;否则通过 common_type 提升 Rep 宽度避免中间溢出。libstdc++ 的 __duration_cast_impl 会根据 Period2::num、Period2::den 与 Period1 的关系选择最优路径:
cpp
// 简化示意:factor = Period1 / Period2 = CF::num / CF::den
// 若 CF::num == 1:count * den(乘法)
// 若 CF::den == 1:count / num(除法)
// 否则:先乘后除,中间用宽类型
因此 duration_cast<milliseconds>(seconds(1)) 在 Release 下通常就是一条乘法指令。
4.4 类型别名:duration_values 与零值
std::chrono::duration_values<Rep> 提供 zero()、min()、max(),用于构造"零时长"或极值,避免依赖 Rep 的字面量(浮点 Rep 的 min() 不是 -max())。
5. time_point 与时钟体系
5.1 time_point 的模板参数
cpp
template<class Clock, class Duration = typename Clock::duration>
class time_point;
默认 Duration 是时钟自己的 duration(如 system_clock::duration 在 MSVC 是 100ns)。但也可以绑定其他 Duration:
cpp
using sys_seconds = std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>;
using sys_days = std::chrono::time_point<std::chrono::system_clock, std::chrono::days>; // C++20 常用
sys_days 是 C++20 日历运算的枢纽------它把 year_month_day(日历表示)与 time_point(线性时间)连接起来。
5.2 时钟间的转换规则
不同时钟的 time_point 不能直接比较或运算(编译错误),但 C++20 提供受控转换:
- clock_cast<ToClock>(tp):在不同时钟间转换(如 system_clock ↔ utc_clock);
- system_clock::to_time_t(tp) / from_time_t(t):与 C 的 time_t 互转;
- time_point_cast<Duration>(tp):同时钟下改变精度。
5.3 时钟的 is_steady 与 epoch
- steady_clock:epoch 是实现定义的(Linux 上是开机时刻;Windows 上是系统启动以来的某个基准),不能转成 time_t,因为它的起点与墙上时间无关。
- system_clock:epoch 是 1970-01-01 00:00:00 UTC(POSIX 约定;Windows FILETIME 内部是 1601,但 MSVC 的 system_clock 对外统一到 1970 纪元)。
- utc_clock:与 system_clock 的差异是闰秒(UTC 含闰秒,system_clock 通常视为不含闰秒的"准 UTC")。
6. 时钟底层实现原理(Windows / Linux)
这是 chrono 最"接地气"的部分:标准库的 now() 最终都落在操作系统的时间源上。
6.1 Windows 平台
| 时钟 | 系统调用/API | tick 周期 | epoch |
|---|---|---|---|
| system_clock | GetSystemTimeAsFileTime(或 GetSystemTimePreciseAsFileTime) | 100 ns(FILETIME 固有精度) | 1601-01-01(内部),对外转 1970 |
| steady_clock | QueryPerformanceCounter + QueryPerformanceFrequency | 取决于硬件(通常 100ns~1μs) | 系统启动后某个基准 |
| file_clock | GetSystemTimeAsFileTime | 100 ns | 1601-01-01 |
QueryPerformanceCounter(QPC)是 Windows 上最高精度的单调计时器:
- 底层依赖硬件:现代 CPU 的 TSC(时间戳计数器)或 HPET/ACPI 电源管理计时器;
- 频率通过 QueryPerformanceFrequency 查询,运行期间恒定;
- 它不受系统时间修改影响,跨 CPU 核心迁移时由 OS 保证一致性(通过 TSC 同步校正)。
MSVC STL 中 steady_clock::now() 的实现大致是:
cpp
// 简化示意
static const long long frequency = [] {
LARGE_INTEGER f;
QueryPerformanceFrequency(&f);
return f.QuadPart;
}();
steady_clock::time_point steady_clock::now() noexcept {
LARGE_INTEGER c;
QueryPerformanceCounter(&c);
// 把 QPC 计数换算为 100ns 或纳秒
return time_point(duration(/* c.QuadPart * 1e9 / frequency */));
}
6.2 Linux 平台
| 时钟 | 系统调用 | clockid | 特性 |
|---|---|---|---|
| system_clock | clock_gettime | CLOCK_REALTIME | 墙上时间,可被 settimeofday/NTP 调整 |
| steady_clock | clock_gettime | CLOCK_MONOTONIC | 单调递增,但受 NTP 频率微调影响 |
| (实现细节) | clock_gettime | CLOCK_MONOTONIC_RAW | 完全不受 NTP 影响(libstdc++ 部分版本用于 steady) |
clock_gettime 是 vDSO(虚拟动态共享对象)加速的系统调用------在用户态直接读取内核映射的时间数据页,通常仅需 20~50ns,不需要陷入内核。这也是为什么 Linux 上高频调用 steady_clock::now() 的开销远低于普通系统调用。
6.3 精度对比
| 时钟 | 典型精度 | 读取开销 | 单调性 |
|---|---|---|---|
| GetTickCount64 | 10~16ms | ~10ns | 单调 |
| GetSystemTimeAsFileTime | 100ns1μs(取决于平台) | ~20-100ns | 否 |
| QueryPerformanceCounter | 硬件级(亚微秒) | ~20-100ns | 单调 |
| clock_gettime(CLOCK_MONOTONIC) | 纳秒级(vDSO) | ~20-50ns | 单调 |
7. 计时实战:性能基准、睡眠与超时
7.1 测量代码执行时间
cpp
#include <chrono>
#include <iostream>
void measure() {
using namespace std::chrono;
auto start = steady_clock::now();
// ... 被测代码 ...
auto end = steady_clock::now();
auto ns = duration_cast<nanoseconds>(end - start);
std::cout << "elapsed: " << ns.count() << " ns\n";
}
要点:
- 必须用 steady_clock,避免系统时间跳变导致负值或异常大值;
- 用 duration_cast<nanoseconds> 显式转换后再 .count();
- 对短于微秒的代码段,单次测量噪声大,应循环多次取最小/平均(这是 Google Benchmark 篇的内容,但这里给出手动版)。
7.2 睡眠与超时
cpp
// 睡眠固定时长
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// 睡眠到指定时刻
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
std::this_thread::sleep_until(deadline);
// 条件变量超时等待
std::condition_variable cv;
std::mutex mtx;
bool ready = false;
{
std::unique_lock<std::mutex> lk(mtx);
cv.wait_for(lk, std::chrono::seconds(2), [] { return ready; });
// 或 cv.wait_until(lk, deadline);
}
sleep_for/sleep_until 底层分别映射到 std::this_thread::sleep_for 的实现(Windows Sleep / Linux nanosleep),而 wait_for 在 wait_until 之上实现(标准要求 wait_for 用 steady_clock 计算绝对时刻,避免系统时间调整影响超时判断)。
7.3 手动实现的轻量定时器
cpp
class Timer {
std::chrono::steady_clock::time_point next_;
std::chrono::milliseconds interval_;
public:
explicit Timer(std::chrono::milliseconds interval)
: next_(std::chrono::steady_clock::now() + interval), interval_(interval) {}
// 返回 true 表示到点;到点后自动续期,保持固定节奏
bool tick() {
auto now = std::chrono::steady_clock::now();
if (now < next_) return false;
next_ += interval_; // 基于上次到期时间累加,避免漂移
return true;
}
};
关键技巧:基于到期时间累加而不是基于 now 累加,这样即使某次处理超时,后续触发点仍保持原节奏(不漂移)。
8. C++20 日历类型与日期算法
C++20 为 chrono 引入了完整的日历与时区支持(<chrono> 头文件新增 year_month_day 等类型)。这是时间库史上最大的一次扩展。
8.1 日历类型族
| 类型 | 语义 | 示例 |
|---|---|---|
| year | 年份(-32767~32767) | 2026y |
| month | 月份(1~12) | 8月 或 August |
| day | 日(1~31) | 26d |
| year_month | 年 + 月 | 2026y / August |
| month_day | 月 + 日 | August / 26d |
| year_month_day | 完整日期 | 2026y / August / 26d |
| year_month_day_last | 月末日(自动处理大小月/闰年) | 2026y / February / last |
| weekday | 星期几(0=Sunday) | Wednesday |
| weekday_indexed | 第几个星期几 | Wednesday2(第二个周三) |
| weekday_last | 最后一个星期几 | Wednesdaylast |
| year_month_weekday | 年 + 月 + 第几个星期几 | 2026y / August / Wednesday2 |
8.2 优雅的构造语法:operator/
日期类型的构造通过重载的 / 运算符链式组合:
cpp
using namespace std::chrono;
auto d1 = 2026y / August / 26d; // year_month_day
auto d2 = 2026y / 8 / 26; // 整数也可
auto d3 = 26d / August / 2026y; // 顺序无关
auto last_feb = 2026y / February / last; // year_month_day_last:2026 年 2 月末(28 日)
auto second_wed = 2026y / August / Wednesday[2]; // 2026 年 8 月第二个周三
这种写法把日期字面量化,可读性远超 tm{26, 0, 0, 26, 7, 126}。
8.3 日期核心算法:civil_from_days / days_from_civil
year_month_day 与 sys_days 之间的转换基于 Howard Hinnant 发表的经典算法(也被 libstdc++/MSVC 采用):
cpp
// 天数 → 日期(civil_from_days)
// 输入:自 1970-01-01 起的天数(可为负)
// 输出:year_month_day
template<class Int>
constexpr year_month_day civil_from_days(Int z) noexcept {
z += 719468; // 移到 0000-03-01 纪元,使 3 月成为一年之首
const Int era = (z >= 0 ? z : z - 146096) / 146097; // 400 年周期(146097 天)
const unsigned doe = static_cast<unsigned>(z - era * 146097); // 纪元内第几天
const unsigned yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365; // 纪元内第几年
const Int y = static_cast<Int>(yoe) + era * 400;
const unsigned doy = doe - (365*yoe + yoe/4 - yoe/100); // 年内第几天
const unsigned mp = (5*doy + 2)/153; // 月(3 月起算)
const unsigned d = doy - (153*mp+2)/5 + 1;
const unsigned m = mp < 10 ? mp+3 : mp-9;
return year_month_day{year{y + (m <= 2)}, month{m}, day{d}};
}
这个算法的精妙之处:
- 把 3 月作为一年的起点,让闰日(2 月 29 日)落在年末,简化计算;
- 用 400 年大周期(146097 天 = 400×365 + 97 闰日)一次性处理闰年规则;
- 全程只用整数加减乘除,无任何循环,constexpr 友好。
反向的 days_from_civil 则是它的逆运算,year_month_day 转 sys_days 时使用。两个算法是日历库的"心脏",任何日期库(Howard Hinnant 的 date 库、libstdc++、MSVC)都基于这一族算法。
8.4 日期运算
cpp
auto d = 2026y / August / 26d;
auto tomorrow = sys_days{d} + days{1}; // 2026-08-27
auto next_month = sys_days{d} + months{1}; // 2026-09-26
auto last_day = sys_days{2026y / February / last}; // 2026-02-28
// 计算两个日期相差多少天
auto diff = (sys_days{2026y / August / 26d} - sys_days{2026y / January / 1d}).count(); // 237
// 星期几
auto wd = weekday{2026y / August / 26d}; // Wednesday
关键区别:
- days 是固定长度(86400 秒),与 sys_days 直接加减;
- months/years 是日历单位,加在 year_month_day 上时按日历规则处理(如 1 月 31 日 + 1 个月 → 2 月 28/29 日,饱和到月末);
- year_month_day 不能直接与 days 加减,必须先转 sys_days(因为日历月长不定)。
8.5 闰年与大小月
year_month_day_last 自动处理:
- 闰年规则:能被 400 整除,或能被 4 整除但不能被 100 整除;
- 2024y / February / last → 2 月 29 日;
- 2026y / February / last → 2 月 28 日。
month 类型自带长度查询:month{8}.length(2026y) → 31。
9. C++20 时区与 zoned_time
9.1 时区数据库
C++20 引入 std::chrono::get_tzdb() / get_tzdb_list() / locate_zone():
cpp
const std::chrono::time_zone* tz = std::chrono::locate_zone("Asia/Shanghai");
- 时区数据库采用 IANA tzdata(Olson 数据库)格式;
- Linux 上通常读取 /usr/share/zoneinfo;
- Windows/MSVC 内置完整 tzdata 副本(首次访问时从系统或随库携带的数据库加载);
- 首次调用 get_tzdb() 会触发数据库加载,可能较慢且非线程安全(标准规定首次调用需数据竞争防护),最佳实践是在程序启动早期单线程调用一次预热。
9.2 zoned_time:本地时间 ↔ UTC
cpp
using namespace std::chrono;
// 从 UTC 时刻构造某个时区的本地时间
auto zt = zoned_time{locate_zone("Asia/Shanghai"), system_clock::now()};
std::cout << zt << '\n'; // 2026-08-26 14:30:00 CST(示例)
// 从本地时间反推 UTC
local_days ld{2026y / August / 26d};
auto sys = zoned_time{locate_zone("Asia/Shanghai"), ld + 14h}.get_sys_time();
zoned_time<Duration, TimeZonePtr> 的核心成员:
- get_sys_time():返回对应 UTC 时刻;
- get_local_time():返回本地时刻;
- 构造时若本地时间在 DST 切换的"间隙"或"重复"区间,会抛出 nonexistent_local_time / ambiguous_local_time 异常。
9.3 夏令时(DST)处理
时区规则包含历史 DST 变化表(何时切换、偏移多少)。time_zone::get_info(sys_time) 返回 sys_info:
cpp
struct sys_info {
sys_seconds begin, end; // 该规则生效区间
seconds offset; // UTC 偏移(含 DST)
bool save; // DST 标志
std::string abbrev; // 时区缩写,如 "CST"、"PDT"
};
例如 Asia/Shanghai 历史上没有 DST(offset 恒为 +8h),而 America/New_York 有春秋切换。库会自动根据历史规则计算任意时刻的偏移,无需手工维护 DST 表。
9.4 时区转换实战
cpp
// 把纽约的本地时间转换为上海的本地时间
auto ny = locate_zone("America/New_York");
auto sh = locate_zone("Asia/Shanghai");
zoned_time ny_time{ny, local_days{2026y / January / 15d} + 12h}; // 纽约本地 12:00
auto sh_time = zoned_time{sh, ny_time.get_sys_time()}; // 转上海
std::cout << sh_time << '\n'; // 次日 01:00 CST(纽约冬季 UTC-5,上海 UTC+8)
10. chrono 格式化与解析
10.1 std::format 支持(C++20)
std::format 原生支持 chrono 类型:
cpp
using namespace std::chrono;
auto now = system_clock::now();
std::cout << std::format("{:%Y-%m-%d %H:%M:%S}", now) << '\n';
// 2026-08-26 14:30:00
std::cout << std::format("{:%F %T %Z}", zoned_time{locate_zone("Asia/Shanghai"), now}) << '\n';
// 2026-08-26 14:30:00 CST
std::cout << std::format("elapsed: {:%T}", now - system_clock::now()) << '\n';
格式说明符与 strftime 基本兼容,常用:
| 说明符 | 含义 | 示例输出 |
|---|---|---|
| %Y | 4 位年份 | 2026 |
| %y | 2 位年份 | 26 |
| %m | 2 位月份 | 08 |
| %d | 2 位日 | 26 |
| %H | 24 小时制小时 | 14 |
| %M | 分钟 | 30 |
| %S | 秒 | 00 |
| %F | 等价 %Y-%m-%d | 2026-08-26 |
| %T | 等价 %H:%M:%S | 14:30:00 |
| %Z | 时区缩写 | CST |
| %z | UTC 偏移 | +0800 |
| %j | 年内第几天 | 238 |
| %A | 完整星期名 | Wednesday |
10.2 std::chrono::parse(C++20)
cpp
std::chrono::sys_seconds tp;
std::string input = "2026-08-26 14:30:00";
std::istringstream is(input);
is >> std::chrono::parse("%Y-%m-%d %H:%M:%S", tp);
// 解析成功则 tp 为对应 UTC 时刻
parse 与 from_stream(流式解析)支持解析到 sys_time、local_time、year_month_day 等类型,是"日志解析、配置读取"等场景的标准方案。
11. 底层实现窥探(libstdc++ / libc++ / MSVC STL)
11.1 duration 的存储
标准要求 duration 只含一个 Rep 数据成员(无额外开销),因此:
cpp
static_assert(sizeof(std::chrono::seconds) == sizeof(long long));
static_assert(sizeof(std::chrono::milliseconds) == sizeof(long long));
duration 是零开销抽象的典范:类型信息(单位)只存在于编译期,运行时就是一个整数/浮点数。
11.2 转换的编译期分派
libstdc++ 的 __duration_cast_impl 用 __is_floating_point 与 ratio 特征在编译期选择算法路径;MSVC 用 _CHRONO 命名空间内的模板特化实现同样目标。无论哪种实现,最终生成的机器码都只有最少的算术指令。
11.3 time_point 的存储
time_point 同样只存一个 Duration:
cpp
static_assert(sizeof(std::chrono::system_clock::time_point) == sizeof(long long));
11.4 C++20 日历类型的实现
year、month、day 都是单整数包装类,用位宽压缩:
- year:内部 short(-32767~32767);
- month:内部 unsigned char(0255,112 有效);
- day:内部 unsigned char;
- year_month_day 在 libstdc++ 中用 days 计数表示(一个 int),转 sys_days 只需一次 days_from_civil 运算。
11.5 时钟 now() 的调用链
steady_clock::now()
→ clock_gettime(CLOCK_MONOTONIC, &ts) // Linux vDSO,用户态完成
→ duration{ts.tv_sec * 1e9 + ts.tv_nsec}
→ time_point{duration}
MSVC 类似:QueryPerformanceCounter → 换算 → time_point。
11.6 constexpr 支持
C++20 起,duration、time_point 的大部分运算、sys_days、year_month_day 的构造与运算都是 constexpr。这意味着日期计算可以在编译期完成:
cpp
constexpr auto d = std::chrono::sys_days{2026y / August / 26d};
static_assert(d.time_since_epoch().count() == 20691); // 编译期计算 1970 起天数
12. 性能对比与最佳实践
12.1 时钟读取开销实测(典型值,随硬件变化)
| 操作 | 典型开销 |
|---|---|
| steady_clock::now()(Linux vDSO) | ~20-50 ns |
| steady_clock::now()(Windows QPC) | ~20-100 ns |
| system_clock::now()(Linux vDSO) | ~20-50 ns |
| system_clock::now()(Windows) | ~50-150 ns |
| time()(C API,秒级) | ~10-20 ns |
| GetTickCount64() | ~10 ns |
最佳实践:
- 高频采样(如性能剖析)优先用 steady_clock;
- 不要用 high_resolution_clock(不保证单调);
- 如果只需要秒级墙上时间,system_clock 足够,别用更高精度(精度越高,通常开销越大且无收益);
- 批量时间戳场景考虑手动缓存(如每秒刷新一次基准,再用廉价计数差值)。
12.2 duration 运算零开销
所有 duration 算术在 Release 下都是纯整数运算,无堆分配、无虚函数、无异常。std::chrono 全库不使用动态内存(时区数据库加载除外)。
12.3 与系统 API 的换算避免
尽量让 duration 保持原始单位,只在 I/O 边界(打印、存储)时转换一次。例如内部用 steady_clock::duration 直接存差值,展示时才 duration_cast。
13. 常见坑点与避坑指南
| 坑点 | 现象 | 避坑方法 |
|---|---|---|
| high_resolution_clock 不保证单调 | 耗时出现负值 | 直接用 steady_clock |
| system_clock 被 NTP/手动改时间 | 测量间隔异常大/负 | 测量间隔用 steady_clock,墙上时间用 system_clock |
| duration_cast 向零截断 | -1.9s → -1s,与预期不符 | 用 floor/ceil/round |
| 隐式转换编译失败 | minutes m = seconds(90) 报错 | 显式 duration_cast |
| 不同时钟 time_point 相减编译错误 | 类型不匹配 | 先统一时钟(clock_cast 或统一来源) |
| steady_clock 转 time_t | 无 API | 语义上不可转,改用 system_clock |
| 浮点 Rep 的精度陷阱 | duration<double> 累计误差 | 整数 Rep 累计,最后再转浮点 |
| months/years 是平均值 | duration_cast<hours>(months{1}) 得到 730h 而非真实月长 | 日历运算用 year_month_day + sys_days |
| 1 月 31 日 + 1 个月 | 饱和到 2 月 28/29(符合日历) | 需要"顺延"语义时自行处理 |
| 时区库首次加载慢/线程安全 | 启动时卡顿、多线程竞态 | 启动早期单线程预热 get_tzdb() |
| DST 切换导致本地时间不存在/重复 | nonexistent_local_time / ambiguous_local_time 异常 | 捕获异常或检查 get_info |
| time_t 32 位溢出(2038 问题) | 2038-01-19 后溢出 | 用 64 位 time_t 或直接用 system_clock::time_point |
| sleep_for 精度受调度影响 | 实际睡眠比请求长 | 需要精确节奏时用 sleep_until 补偿或忙等(不推荐) |
| 打印 duration 直接 .count() | 输出裸整数,无单位 | 先 duration_cast 到目标单位并附带单位字符串,或 C++20 用 std::format("{:%T}", d) |
14. FAQ 速查表
Q1:测量代码耗时用哪个时钟? A:std::chrono::steady_clock。不要用 high_resolution_clock(不保证单调),更不要用 system_clock(会被系统时间调整影响)。
Q2:steady_clock 和 system_clock 有什么区别? A:steady_clock 单调递增、不受系统时间修改影响,适合测量间隔,但 epoch 与墙上时间无关、不能转 time_t;system_clock 反映 UTC 墙上时间,可转 time_t/日历,但不单调。
Q3:为什么 minutes m = seconds(90) 编译失败? A:隐式转换要求无损。90 秒不能无损表示为整数分钟,必须显式 duration_cast<minutes>(得到 1 分钟,截断 30 秒)。
Q4:duration_cast、floor、ceil、round 有什么区别? A:duration_cast 向零截断;floor 向下取整;ceil 向上取整;round 四舍五入(远离零)。负值场景四者结果不同。
Q5:如何把 chrono 时间转成字符串? A:C++20 用 std::format("{:%Y-%m-%d %H:%M:%S}", tp);C++17 及以下先 system_clock::to_time_t 再用 std::put_time/strftime。
Q6:如何解析字符串为时间? A:C++20 用 std::chrono::parse("%Y-%m-%d", tp) 或流式 from_stream。
Q7:2026-01-31 加一个月是什么? A:sys_days{2026y/January/31d} + months{1} 的日历语义是 2026-02-28(饱和到月末)。需要"顺延"到 3 月 3 日之类需自行处理。
Q8:chrono 有性能开销吗? A:运行时零额外开销------duration/time_point 就是单个整数,运算就是整数运算。时区数据库加载除外。
Q9:如何跨时区转换时间? A:zoned_time{locate_zone("Asia/Shanghai"), tp}.get_sys_time() 得到 UTC,再用目标时区构造 zoned_time 即可。
Q10:chrono 支持闰秒吗? A:utc_clock 支持(含闰秒表);system_clock 通常视为不含闰秒的 UTC(与 POSIX 一致)。
Q11:sleep_for 和 sleep_until 用哪个? A:需要"至少睡到某时刻"用 sleep_until(绝对时刻,不受中途唤醒累积误差影响);简单睡眠用 sleep_for。
Q12:条件变量超时等待用 wait_for 还是 wait_until? A:wait_for 内部基于 steady_clock 换算成 wait_until,两者语义一致;需要"绝对截止时间"(如轮询周期任务)用 wait_until 更清晰。
Q13:如何判断某个年份是否闰年? A:std::chrono::year{2024}.is_leap()。
Q14:sys_days 是什么? A:time_point<system_clock, days>,以天为单位的系统时钟时刻,是 year_month_day 与 time_point 之间的桥梁。
Q15:chrono 可以用于性能基准吗? A:可以(steady_clock + 多次测量取最小),但严谨的基准测试建议用 Google Benchmark(本系列已有专篇),它处理了预热、统计、防止优化等细节。