C++ std::chrono 时间库深度解析:从 duration 模板到 C++20 日历与时区

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(本系列已有专篇),它处理了预热、统计、防止优化等细节。

相关推荐
Doraemomo22 分钟前
Linux编程-epoll多路IO复用
linux·运维·服务器
kmomo..33 分钟前
Linux 进程学习笔记
linux·笔记·学习
Tanner_SL44 分钟前
Linux笔记之PATH, LD_LIBRARY_PATH和LIBRARY_PATH的区别
linux·运维·笔记
nike0good1 小时前
CF 126B(Password-z algorithm/exkmp)
开发语言·c++·算法
Mapleay2 小时前
Linux 内核调试
linux
从零开始的嵌入式之旅2 小时前
day32
linux·c语言·经验分享·笔记·嵌入式硬件
秃秃秃秃哇2 小时前
orin上sensor一帧raw图大小的计算
linux
01_ice2 小时前
C++入门基础
c++
byte轻骑兵2 小时前
【BlueZ 】蓝牙配对/绑定基础:BlueZ 中安全机制的入门级源码解析
linux·人工智能·bluez·电脑蓝牙·嵌入式蓝牙