chrono 完全指南:类型安全的时间处理库
chrono 完全指南:类型安全的时间处理库
C 语言的 time_t + struct tm 组合在处理时间时几乎没有任何类型安全可言:时间戳是 long、时间间隔是 double 秒、月份从 0 开始、时区转换依赖全局 TZ 环境变量。std::chrono 从 C++11 起步,用一套精密的类型系统彻底终结了这一切——它把时间单位编码到类型里,让"把秒当成毫秒传进函数"这类错误变成编译错误而非凌晨三点的 P0 事故。
C++20 是 chrono 的第二个里程碑:日历类型、时区数据库、std::format 支持和基于 chrono::parse 的字符串解析,让 chrono 从"底层基础库"一跃成为"日常时间处理的首选工具"。
1. 设计哲学:编译期类型安全
chrono 的核心设计可以浓缩为一句:让编译器帮你做单位换算。
// C 风格:所有时间间隔都是同一个类型,错误在运行时才暴露
double timeout_seconds = 0.5;
double interval_ms = 500;
if (timeout_seconds < interval_ms) { /* bug:单位不一致,编译器沉默 */ }
// chrono:不同单位 = 不同类型,错误在编译期暴露
auto timeout = 500ms; // std::chrono::milliseconds
auto interval = 500us; // std::chrono::microseconds
// auto cmp = timeout < interval; // C++11/14: 需要显式转换
auto cmp = timeout < interval; // C++17: CTAD + 隐式转换,OKchrono 有三个核心组件:
- duration:时间间隔。"有多长"——编译期比率的整型运算。
- time_point:时间点。"什么时候"——一个 duration 加上一个时钟纪元。
- clock:时钟。"按什么标准走的"——提供 epoch 和 tick 速率,把 duration 锚定到物理世界。
这三者形成了一条严密的类型链:clock → time_point<clock> → duration。每一层都是一种类型约束,防止你把 system_clock::time_point 误传给期待 steady_clock::time_point 的函数。
2. duration:编译期有理数
std::chrono::duration<Rep, Period> 只有两个模板参数:
Rep:表示值的算术类型(int64_t、double等)Period:一个std::ratio,表示每秒的 tick 数
using nanoseconds = duration<int64_t, std::nano>; // 1 tick = 1ns
using milliseconds = duration<int64_t, std::milli>; // 1 tick = 1ms
using seconds = duration<int64_t>; // Period 默认是 ratio<1>
using minutes = duration<int64_t, std::ratio<60>>;
using hours = duration<int64_t, std::ratio<3600>>;2.1 duration_cast 与浮点 duration
duration 之间的转换分为两种:
- 隐式转换:窄→宽,无信息丢失(
milliseconds→seconds是ratio<1,1000>→ratio<1>,整除会截断,所以不允许隐式) - 显式转换:
duration_cast,等价于static_cast,直接截断 - 浮点转换:使用
duration<double>做中介
auto t1 = 1500ms; // duration<int64_t, milli>
auto t2 = duration_cast<seconds>(t1); // 1s (截断)
auto t3 = duration<double>(t1); // 1500.0ms (浮点)
auto t4 = duration_cast<seconds>(t3); // 1s (浮点 → 整型仍然截断)
// 四舍五入?用 C++17 的 round
auto t5 = round<seconds>(1500ms); // 2s
auto t6 = round<seconds>(1499ms); // 1s
// 向上/向下取整
auto t7 = floor<seconds>(1999ms); // 1s
auto t8 = ceil<seconds>(1001ms); // 2s2.2 字面量 (C++14)
using namespace std::chrono_literals;
auto t1 = 1h + 30min + 15s + 500ms; // 纯自然语法
auto t2 = 3.14s; // duration<long double>,浮点秒
std::this_thread::sleep_for(100ms); // 一目了然
// std::this_thread::sleep_for(0.1); // C 风格:是秒?毫秒?天?字面量内部直接调用对应的 duration 构造函数,类型明确,没有隐式开销。
2.3 duration 的算术规则
duration 支持所有六种比较运算符和 +、-、*、/、%:
auto total = 2h + 30min; // duration<int64_t, ratio<60>> = 150min
auto diff = 10s - 500ms; // duration<int64_t, milli> = 9500ms
auto ratio = 1s / 1ms; // int64_t = 1000(不是 duration!)
auto half = 1s / 2; // duration<int64_t> = 0s(整型除法)
auto half_f = duration<double>(1s) / 2; // duration<double> = 0.5s
auto mod = 10s % 3s; // duration<int64_t> = 1s关键洞见:duration 之间相除得到的是一个纯数值——这是在类型维度上做消去,秒/毫秒 把两边的时间单位消掉,剩下 1000.0。除法得到 duration 只在除以纯数值时发生。这个设计自然到你可能不会注意,但它彻底防止了"把角度当时间传进去"这类离谱错误。
3. time_point:epoch + duration
template<class Clock, class Duration = typename Clock::duration>
class time_point;一个 time_point 就是一个 duration 加上它所相对于的 epoch(由 Clock 定义)。两个 time_point 相减得到 duration,time_point + duration 得到新的 time_point:
auto start = std::chrono::steady_clock::now();
do_work();
auto end = std::chrono::steady_clock::now();
auto elapsed = end - start; // duration,类型由 clock 决定
std::cout << elapsed.count() << "ns\n"; // count() 返回原始数值不同 clock 的 time_point 之间不能互操作——编译期直接拒绝。system_clock::time_point 和 steady_clock::time_point 是完全不同的类型,不能相减,不能比较。这是刻意为之——它们的 epoch 和 tick 速率不同,让编译器拦截比"运行时得到无意义的结果"好得多。
3.1 time_point_cast
auto tp1 = std::chrono::system_clock::now(); // 典型精度:nanoseconds
auto tp2 = time_point_cast<seconds>(tp1); // 截断到秒级与 duration_cast 相同,time_point_cast 对底层的 duration 做转换。floor/ceil/round 同样适用。
4. 时钟:把 duration 锚定到物理世界
4.1 C++11 时钟
| 时钟 | epoch | 特性 | 典型用途 |
|---|---|---|---|
system_clock | 1970-01-01 (Unix epoch) | 可和 time_t 互转,可被 NTP/用户调整 | 日历时间、日志时间戳 |
steady_clock | 实现定义(通常开机时间) | 单调递增,从不回退 | 性能测量、超时计时 |
high_resolution_clock | 实现定义 | 通常是 system_clock 或 steady_clock 的 typedef | 不要直接用 |
high_resolution_clock 是设计失误
high_resolution_clock 在不同的标准库实现中可能是 system_clock 的别名(libstdc++)也可能是 steady_clock 的别名(libc++/MSVC)。你的程序在 Linux 上编译通过,换到 macOS 可能因为"不是 steady"而导致测时出现负值。永远不要用它。需要 wall-clock 用 system_clock,需要测时间间隔用 steady_clock。
4.2 system_clock 的 to_time_t / from_time_t
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::cout << std::ctime(&t); // 传统 C API 互操作
auto tp = std::chrono::system_clock::from_time_t(t); // 反向转换精度损失警告:time_t 通常是秒级整数,system_clock 的精度通常是纳秒。to_time_t 会截断(实际上是 floor<seconds>),微秒/纳秒信息丢失。
4.3 C++20 新增时钟
C++20 引入了四个新时钟,全部基于 TAI(国际原子时间)体系:
// utc_clock: 带闰秒的 UTC。与 system_clock 相差闰秒偏移
auto utc_now = std::chrono::utc_clock::now();
// tai_clock: 国际原子时间。TAI = UTC + 37s (截至 2017)
auto tai_now = std::chrono::tai_clock::now();
// gps_clock: GPS 时间。GPS = UTC + 18s (减去 UTC 偏移,加上 19s GPS-UTC 偏差)
auto gps_now = std::chrono::gps_clock::now();
// file_clock: 文件系统时间。用于 std::filesystem::file_time_type
auto ftime = std::chrono::file_clock::now();这些时钟之间支持 clock_cast 互转(正确处理闰秒和 epoch 差异):
auto sys_tp = std::chrono::system_clock::now();
auto utc_tp = std::chrono::clock_cast<std::chrono::utc_clock>(sys_tp);
auto tai_tp = std::chrono::clock_cast<std::chrono::tai_clock>(sys_tp);
// clock_cast 自动处理 epoch 偏移和闰秒
assert(tai_tp.time_since_epoch() - utc_tp.time_since_epoch() == 37s); // (当前差值)5. C++20 日历类型:告别 struct tm
C++20 的日历类型是 chrono 最激动人心的扩展。它用一套正交的类型系统替代了 C 的 struct tm:
using namespace std::chrono;
// 基础类型(全部是值语义,复制成本是一个 int)
year y{2024};
month m{7};
day d{30};
weekday wd{Saturday}; // 星期几
month_day md{July/30}; // month/day 组合
year_month_day ymd{2024y/July/30}; // 完整日历日期
// 访问 today()
auto today = year_month_day{
floor<days>(system_clock::now())
}; // 2024y/July/305.1 日历运算
关键优势:日期的加减直接产出正确结果,跨越月/年边界自动处理:
auto date = 2024y/December/31;
auto next = date + months{1}; // 2025y/January/31
auto prev = date - months{1}; // 2024y/November/30
// year_month_day 在构造时自动校验
auto last_day = 2024y/February/29d; // OK: 2024 是闰年
// auto bad = 2023y/February/29d; // 编译错误!不是 constexpr
// (实际上 chrono 在某些实现中会抛出 exception,取决于构造函数选择)
// 获取本月最后一天
auto last = 2024y/February/last; // year_month_day_last = 2024-02-295.2 星期运算
auto date = 2024y/January/1; // Monday
auto wd = weekday{date}; // Monday
// 找本月第一个周日
auto first_sun = 2024y/January/Sunday[1]; // 2024-01-07
// 找本月最后一个周五
auto last_fri = 2024y/January/Friday[last]; // 2024-01-26
// 获取星期几
std::cout << wd << '\n'; // "Mon" (需要 C++20 format)5.3 日时长 (hh_mm_ss)
std::chrono::hh_mm_ss 把 duration 拆分为时分秒毫秒微秒的易读形式:
auto t = 14h + 30min + 25s + 500ms + 123us;
auto hms = hh_mm_ss{t}; // 拆分
hms.hours(); // 14h
hms.minutes(); // 30min
hms.seconds(); // 25s
hms.subseconds(); // duration (500ms + 123µs) 合并表示
// 负 duration 的处理
auto neg = hh_mm_ss{-2h + 30min}; // -1h -30min
// hours() = -1h, minutes() = -30min, 全部为负6. C++20 时区支持
chrono 的时区功能依赖 IANA 时区数据库。编译器/标准库需要访问到这些时区文件。GCC/libstdc++ 目前尚未完整实现 std::chrono::tzdb(写本文时),但可以通过 Howard Hinnant 的 date 库获得等价功能。在完成实现的平台上(MSVC, libc++),用法如下:
// 获取时区数据库
const auto& tzdb = std::chrono::get_tzdb();
auto& ny_tz = tzdb.current_zone(); // 当前系统时区
// 创建带时区的时间
auto ny_time = std::chrono::zoned_time{
"America/New_York",
std::chrono::system_clock::now()
};
// 转换为其他时区
auto tokyo_time = std::chrono::zoned_time{
"Asia/Tokyo",
ny_time
};
// 获取本地时间部分
auto local = ny_time.get_local_time(); // year_month_day + hh_mm_ss
auto sys = ny_time.get_sys_time(); // system_clock::time_point (UTC)6.1 时区转换与歧义
DST 过渡期间可能出现不存在的本地时间(春季拨快 1 小时)或出现两次(秋季拨慢 1 小时):
// 2024-03-10 02:30:00 America/New_York 不存在(春季 DST 跳到 03:00)
auto ambiguous = std::chrono::local_days{2024y/March/10} + 2h + 30min;
try {
auto zt = std::chrono::zoned_time{"America/New_York", ambiguous};
} catch (const std::chrono::nonexistent_local_time&) {
// 这个时间不存在
}
// 秋季回拨期间 choose::earliest / choose::latest
using std::chrono::choose;
auto fall = std::chrono::local_days{2024y/November/3} + 1h + 30min;
auto early = std::chrono::zoned_time{"America/New_York", fall, choose::earliest};
auto late = std::chrono::zoned_time{"America/New_York", fall, choose::latest};7. 格式化与解析 (C++20)
7.1 std cout 格式化
auto now = std::chrono::system_clock::now();
// 基本格式化
std::cout << std::format("{:%Y-%m-%d %H:%M:%S}", now);
// → "2024-07-30 14:30:25"
// 完整 ISO 8601
std::cout << std::format("{:%FT%T%z}", now);
// 自定义精度
std::cout << std::format("{:%H:%M:%S}", floor<seconds>(now));
// → "14:30:25" (截断到秒)格式说明符与 strftime 兼容,但有扩展。%S 在 chrono 中可以带小数秒(14:30:25.123456789),而 strftime 的 %S 只能是整数秒。
7.2 流输出
C++20 也为 operator<< 增加了 chrono 支持(虽然 std::format 通常是更好的选择):
std::cout << hh_mm_ss{14h + 30min + 25s + 500ms} << '\n';
// → "14:30:25.500"
std::cout << 2024y/July/30 << '\n';
// → "2024-07-30"
// 注意:流输出的性能不如 format(见《libstdc++ chrono ostream 优化补丁》)7.3 解析 (std parse)
// 从字符串解析时间
std::istringstream iss("2024-07-30 14:30:25");
std::chrono::sys_seconds tp; // time_point<system_clock, seconds>
std::chrono::from_stream(iss, "%F %T", tp);
// tp = 2024-07-30 14:30:25 UTC
// 或者用 parse (更简洁)
tp = {}; // 重置
std::chrono::parse("%F %T", "2024-07-30 14:30:25", tp);
// 解析带时区
std::chrono::zoned_time<std::chrono::seconds> zt;
std::chrono::parse("%F %T %Z", "2024-07-30 14:30:25 America/New_York", zt);8. 常见模式与最佳实践
8.1 性能测量
template<typename F, typename... Args>
auto measure(F&& f, Args&&... args) {
auto start = std::chrono::steady_clock::now();
if constexpr (std::is_void_v<decltype(f(args...))>) {
f(std::forward<Args>(args)...);
auto end = std::chrono::steady_clock::now();
return end - start;
} else {
auto result = f(std::forward<Args>(args)...);
auto end = std::chrono::steady_clock::now();
return std::make_pair(result, end - start);
}
}
// 使用
auto elapsed = measure(do_work); // 返回 duration
std::cout << elapsed << '\n'; // "1234567ns"要点:
- 使用
steady_clock(单调,不会因 NTP 回拨而出负值) - 用
auto推导 duration 类型,避免无意中截断精度 - 需要打印时再用
duration_cast或std::format
8.2 超时与截止时间
// 风格一:相对超时(duration)
bool wait_for_data(int timeout_ms) {
std::unique_lock lock(mutex_);
return cv_.wait_for(lock, std::chrono::milliseconds(timeout_ms),
[this] { return data_ready_; });
}
// 风格二:绝对截止时间(time_point)—— 推荐
bool wait_until_deadline(std::chrono::steady_clock::time_point deadline) {
std::unique_lock lock(mutex_);
return cv_.wait_until(lock, deadline,
[this] { return data_ready_; });
}
// 为什么用 time_point?
// wait_for 被虚假唤醒后需要重新等待——如果每次都新建相对时间,
// 总等待时间会累积误差。而 time_point 是绝对的,不受重试影响。8.3 年龄/过期计算
// 检查某个时间戳是否超过 30 天
template<typename TimePoint>
bool is_expired(TimePoint tp, std::chrono::days max_age = std::chrono::days{30}) {
auto age = std::chrono::system_clock::now() - tp;
return age > max_age;
}
// 或者用 floor——对"日历天"语义更准确
bool is_expired_calendar(std::chrono::system_clock::time_point tp,
std::chrono::days max_age = std::chrono::days{30}) {
auto age_in_days = std::chrono::floor<std::chrono::days>(
std::chrono::system_clock::now() - tp);
return age_in_days > max_age;
}8.4 定时器:轮询间隔与修正
// 朴素但有漂移的循环
for (int i = 0; i < 10; ++i) {
do_work();
std::this_thread::sleep_for(100ms); // do_work() 耗时导致漂移
}
// 修正版:基于绝对时间
auto next = std::chrono::steady_clock::now();
constexpr auto interval = 100ms;
for (int i = 0; i < 10; ++i) {
do_work();
next += interval;
std::this_thread::sleep_until(next); // 不受 do_work() 耗时影响
}9. 常见陷阱
9.1 steady_clock vs system_clock
┌─────────────────────────────────────────────────────────────────┐
│ 场景 │ system_clock │ steady_clock │
├─────────────────────────────────────────────────────────────────┤
│ 日历时间(年月日时分秒) │ ✅ │ ❌ epoch 是实现定义的 │
│ 日志时间戳 │ ✅ │ ❌ │
│ 与 time_t 互转 │ ✅ │ ❌ │
│ 性能测量(间隔) │ ❌ 可能回退 │ ✅ 单调递增 │
│ 超时的 deadline │ ❌ │ ✅ │
│ 定时器(sleep_until) │ ❌ │ ✅ │
│ 序列化/持久化 │ ✅ │ ❌ │
│ 与外部系统对接 │ ✅ │ ❌ │
└─────────────────────────────────────────────────────────────────┘经验法则:一个变量叫 now,问它"now of what?" 如果是"现在的日历时间",用 system_clock::now();是"现在相比于某个操作开始的时刻",用 steady_clock::now()。
9.2 duration 溢出的静默危险
// nanoseconds 用 int64_t 表示,最大值 = 9.22e18 ns ≈ 292 年
// 够吗?通常够。但如果做乘法:
auto t = std::chrono::hours{100000} * 100000; // 溢出!
// 100000h = 3.6e11 * 1e9 ns = 3.6e20 ns > 9.22e18 → UB
// 防御:用更大位宽或浮点
auto t_safe = std::chrono::duration<double>(std::chrono::hours{100000}) * 100000;
// 或者用 C++26 的 std::chrono::days (int32_t) 时注意范围9.3 不要把 duration 当数值运算后直接丢给 sleep
auto delay = 500ms;
std::this_thread::sleep_for(delay * 2); // OK: 1000ms
// 但如果 delay 是浮点
auto f_delay = 500.0ms; // duration<double, milli>
std::this_thread::sleep_for(f_delay); // OK: sleep_for 接受任意 Rep
// 更好的写法:始终用整型 duration
auto delay_i = std::chrono::round<std::chrono::milliseconds>(500.0ms);
// 或直接用字面量保证类型
using namespace std::chrono_literals;
auto delay_x = 500ms; // 永远是 duration<int64_t, milli>9.4 zoned_time 的构造开销
zoned_time 的构造需要查找时区数据库——首次调用 get_tzdb() 会加载整个 IANA 数据库(~200KB 解析开销)。在性能敏感路径中,缓存 zoned_time 或使用 time_zone* 指针而非反复构造成本更低:
// 避免
for (const auto& event : events) {
auto zt = std::chrono::zoned_time{"Asia/Shanghai", event.timestamp};
format_event(zt);
}
// 更好:外部缓存 time_zone*
const auto* shanghai = std::chrono::locate_zone("Asia/Shanghai");
for (const auto& event : events) {
auto zt = std::chrono::zoned_time{shanghai, event.timestamp};
format_event(zt);
}9.5 clock_cast 只存在于 C++20
C++11/14/17 只有 system_clock 和 steady_clock,它们之间不能互转——epoch 不同、语义不同。clock_cast 是 C++20 为 UTC/TAI/GPS/file_clock 等设计的新设施,它不适用于 steady_clock。steady_clock 的 epoch 是实现定义的(通常是系统启动时间),与任何其他时钟都没有固定的偏移。
10. 选型速查
| 需求 | 类型/函数 | 说明 |
|---|---|---|
| 表示一段时间 | std::chrono::milliseconds 等 | 用具体类型,不用 duration<double> 除非确实需要浮点 |
| 性能计时 | steady_clock::now() + 相减 | 单调保证 |
| 日志/日历时间 | system_clock::now() | 可转 time_t,可格式化 |
| 日期运算 | year_month_day + months{} / years{} | 自动处理边界 |
| 星期几 / 第N个X曜日 | weekday{ymd} / Index[weekday] | 原生支持 |
| 时间戳转字符串 | std::format("{:%Y-%m-%d}", tp) | C++20 |
| 字符串转时间 | std::chrono::parse("%F %T", str, tp) | C++20 |
| 时区转换 | std::chrono::zoned_time | C++20,需 IANA DB |
| 跨 GPS/TAI/UTC 转换 | std::chrono::clock_cast | C++20 |
| sleep / wait | sleep_for(duration) / sleep_until(time_point) | 后者更精确 |
| duration 转数值 | .count() | 仅在需要与 C API 互操作时使用 |
| 截断/取整 | duration_cast / floor / ceil / round | 显式选择语义 |
| 拆分为时分秒 | hh_mm_ss<Duration> | C++20 |
总结
std::chrono 经历了从 C++11 的"基础框架"到 C++20 的"全功能日历+时区库"的进化。它最核心的设计原则——把物理单位编码到类型系统里——在时间处理领域是一次彻底的范式转移。
使用 chrono 的基本原则:
- 任何时间间隔都用 duration 类型,不要用裸
int/double - 区分
system_clock(日历时间)和steady_clock(间隔测量) - C++20 的日历类型替代
struct tm,时区支持替代TZ环境变量 std::format替代strftime,std::chrono::parse替代strptime- 优先使用
time_point做截止时间语义,而非累积duration(防止漂移)
掌握 chrono 不只是学一个新库——它是让你从"凑合能跑"的时间处理上升到"类型保证正确"的时间处理。当编译器帮你拦截了第 10 个"毫秒当秒"的 bug 时,你会发现这一切的投资都是值得的。