)
前言std::chrono::duration时长是 C 时间库的地基它把「一个数」和「这个数的单位」绑成了一个编译期类型。换算关系不在运行期做判断而是在模板实例化时就由类型系统确定因此1s 500ms能得到正确结果而1s 500这种句子根本写不出来。最常见的误解是以为duration内部存的是「秒」或者「秒的浮点数」。不是。它存的是一个计数count和两个模板参数计数类型Rep以及一个用std::ratio表示的一个 tick 等于多少秒Period。std::chrono::seconds不是「一秒的 double」而是duration某个至少 64 位的有符号整数, std::ratio1,1——一个整数计数。这个区别决定了很多看起来该编过的代码编不过。第二个常见误解是把duration_cast当成「类型转换」随手用忽略了它是截断truncation而不是四舍五入负数时的方向也不是「向下取整」。本文先用两节把duration的机制拆开——模板参数的含义、std::ratio的算术、隐式转换与显式转换的分界——然后用一节从零复现一个可编译的my_duration最后讲精度、溢出与时钟选择上的实际坑。说明本文以 C17 为基准。示例中用到 C20 或更新的设施如std::chrono::days、hh_mm_ss、chrono 的流输出时会明确标注。std::chrono::nanoseconds这类预定义别名的确切整数类型是实现定义的libstdc、libc 与 MSVC STL 并不一致本文涉及处都会点明。一、骨架两个模板参数与 std::ratio标准里的声明形式是chrono头文件namespace std { namespace chrono { template class Rep, class Period std::ratio1 class duration; }}Rep计数类型通常是有符号整数也允许是浮点。Periodstd::ratioNum, Den类型的特化含义是「一个 tick 代表 Num/Den 秒」。例如毫秒的Period是std::ratio1, 1000一分钟是std::ratio60, 1。std::ratio本身就是一个只带两个static constexpr成员的编译期分数namespace std { template std::intmax_t Num, std::intmax_t Den 1 struct ratio { static constexpr std::intmax_t num /* 约分后的分子 */; static constexpr std::intmax_t den /* 约分后的分母 */; }; }关键在「约分」std::ratio60, 120与std::ratio1, 2是同一个类型因为ratio在实例化时会用最大公约数约分。这个性质让duration的模板参数可以放心做类型比较。duration提供的成员函数不多但每一个都值得记住成员形式说明countconstexpr Rep count() const;取出裸计数单位信息在这里丢失zerostatic constexpr duration zero();计数为 0min/maxstatic constexpr duration min()/max();取 Rep 的lowest()/max()rep类型别名计数类型period类型别名std::ratio特化预定义别名方面标准并不规定统一的整数类型而是给每个别名规定了一个最小位宽具体用哪个整数类型由实现决定别名标准规定的最小位宽有符号整数实现选择举例非标准强制nanoseconds至少 64 位libstdc64 位平台上longMSVC STLlong longmicroseconds至少 55 位同上各实现自行选择milliseconds至少 45 位同上seconds至少 35 位同上minutes至少 29 位有的实现在 32 位int上就够用hours至少 23 位同上上表右列的long/long long/int是实现细节这一点必须当真写模板时按std::chrono::milliseconds::rep去取类型永远是对的硬编码long long就可能在某些平台上出错同时也不要假设「秒的计数类型」和「毫秒的计数类型」一定相同。二、隐式转换与 duration_cast 的分界duration的转换构造函数的约束是整个类的灵魂。标准 [time.duration.cons] 规定从durationRep2, Period2到durationRep, Period的转换构造只有在不丢精度时才隐式参与重载判据是两条同时成立或其中一条特例成立Rep是浮点std::chrono::treat_as_floating_pointRep为真或者std::ratio_dividePeriod2, Period::den 1且Rep2不是浮点。ratio_divideA, B的语义是「A 除以 B」即分子A::num * B::den、分母A::den * B::num再约分。分母为 1 意味着源单位是目标单位的整数倍也就是「粗→细」方向不丢精度。转换ratio_divide的 den允许隐式milliseconds→seconds1000❌ 丢精度必须duration_castseconds→milliseconds1✅ 允许int型seconds→double型seconds1✅ 允许Rep 是浮点double型seconds→int型seconds1❌ Rep 是整数、Rep2 是浮点禁止minutes→seconds1✅ 允许#include chrono int main() { std::chrono::milliseconds ms(1500); // std::chrono::seconds bad ms; // ❌ 编译失败丢精度 std::chrono::seconds good std::chrono::duration_caststd::chrono::seconds(ms); std::chrono::seconds s(2); std::chrono::milliseconds msec s; // ✅ 隐式不丢精度 (void)good; (void)msec; return 0; }而std::chrono::duration_cast的价值在于它显式承诺接受损失其标准化的计算式[time.duration.cast]为CF ratio_divide源 Period, 目标 Period CR common_type_t源 Rep, 目标 Rep, intmax_t 结果 目标Rep( static_castCR(d.count()) * static_castCR(CF::num) / static_castCR(CF::den) )注意最后一步是整数除法——所以它是向零截断不是四舍五入也不是向下取整。取整方向的问题在负数上会暴露得很明显稍后讲。三、从零复现一个 my_duration下面这份实现刻意贴近标准结构只要ratio、type_traits、numeric、cstdint能完整编译。// my_chrono.hpp —— 需要 C17用到 std::gcd / std::lcm / std::common_type_t #include cstdint #include numeric #include ratio #include type_traits // 对应标准里的 std::chrono::treat_as_floating_point默认按 is_floating_point 判断 template class Rep struct my_floating : std::is_floating_pointRep {}; template class Rep, class Period std::ratio1 class my_duration { public: using rep Rep; using period Period; // 必须是一个 std::ratio 特化 // 默认构造计数为 0 constexpr my_duration() : count_(0) {} my_duration(const my_duration) default; my_duration operator(const my_duration) default; // 隐式转换构造只在不丢精度时参与重载 template class Rep2, class Period2, class std::enable_if_t std::is_convertibleconst Rep2, Rep::value (my_floatingRep::value || (std::ratio_dividePeriod2, Period::den 1 !my_floatingRep2::value)) constexpr my_duration(const my_durationRep2, Period2 d) : count_(static_castRep(d.count())) {} // 显式计数构造duration(1000) 这种写法 template class Rep2, class std::enable_if_t std::is_convertibleconst Rep2, Rep::value (my_floatingRep::value || !my_floatingRep2::value) constexpr explicit my_duration(const Rep2 r) : count_(static_castRep(r)) {} constexpr Rep count() const { return count_; } static constexpr my_duration zero() { return my_duration(); } // 标准还提供 min() / max()分别取 numeric_limitsRep::lowest() 与 max() private: Rep count_; }; // 显式精度转换公式与 [time.duration.cast] 一致 template class ToDuration, class Rep, class Period constexpr ToDuration my_duration_cast(const my_durationRep, Period d) { using CF std::ratio_dividePeriod, typename ToDuration::period; using CR std::common_type_tRep, typename ToDuration::rep, std::intmax_t; return ToDuration(static_casttypename ToDuration::rep( static_castCR(d.count()) * static_castCR(CF::num) / static_castCR(CF::den))); } // 两个 duration 的公共类型Rep 取 common_typePeriod 取 num 的 gcd 与 den 的 lcm // 这与标准对 std::common_typeduration, duration 的特化一致 template class R1, class P1, class R2, class P2 using my_common_duration_t my_durationstd::common_type_tR1, R2, std::ratiostd::gcd(P1::num, P2::num), std::lcm(P1::den, P2::den); template class R1, class P1, class R2, class P2 constexpr my_common_duration_tR1, P1, R2, P2 operator(const my_durationR1, P1 lhs, const my_durationR2, P2 rhs) { using CD my_common_duration_tR1, P1, R2, P2; using R typename CD::rep; return CD(static_castR(my_duration_castCD(lhs).count() my_duration_castCD(rhs).count())); }配一个完整可编译的main#include iostream // 接上面的 my_chrono.hpp int main() { using my_milliseconds my_durationlong long, std::milli; using my_seconds my_durationlong long, std::ratio1; using my_minutes my_durationlong long, std::ratio60; my_milliseconds frame(33); my_seconds clip(90); // 公共单位是毫秒gcd(1,1)/lcm(1000,1) → ratio1,1000 auto sum frame clip; std::cout sum.count() ms\n; // 90033 my_seconds truncated my_duration_castmy_seconds(sum); std::cout truncated.count() s\n; // 90截断掉 33ms my_milliseconds roundtrip clip; // 隐式粗 → 细安全 std::cout roundtrip.count() ms\n; // 90000 my_minutes mm my_duration_castmy_minutes(clip); std::cout mm.count() min\n; // 190s 截断成 1min return 0; }逐点核对frame clip中frame的Period是ratio1,1000clip的是ratio1,1gcd(1,1) 1lcm(1000,1) 1000所以公共Period是ratio1,1000也就是毫秒结果 33 90000 90033。my_duration_castmy_seconds的CF为ratio_divideratio1,1000, ratio1,1即ratio1,1000num/den为 1/1000于是 90033 * 1 / 1000 在整数除法下得 90正是向零截断的结果。四、精度、取整方向与时钟选择取整方向。上面my_duration_cast与标准的duration_cast都是向零截断。这在负数上会反直觉#include chrono #include iostream int main() { using namespace std::chrono; auto d milliseconds(-1500); std::cout duration_castseconds(d).count() \n; // -1向零截断 std::cout floorseconds(d).count() \n; // -2向下取整 std::cout duration_castmilliseconds(duration_castseconds(d)).count() \n; // -1000误差 500ms return 0; }floor/ceil/round对duration的重载是 C17 新增的chrono要求 C17C17 之前只有对time_point的版本。它们和duration_cast的差别正是取整方向round是四舍五入.5 向远离零的方向floor向下ceil向上。溢出。std::int32_t计数的毫秒最多表示约 24.8 天2147483647 毫秒超过就是有符号整数溢出——这是未定义行为UB标准不保证任何结果。复现的my_duration里用std::intmax_t做中间类型能缓解但不能根治有些方向的换算仍可能溢出intmax_t一旦溢出同样是 UB。时钟选择。std::chrono::system_clock表示「墙上时间」它可被 NTP 校正、可被用户改两次now()的差可能为负测时间间隔必须用std::chrono::steady_clock标准要求它单调递增且速率恒定。steady_clock的精度resolution是实现定义的libstdc 在多数 Linux 上基于clock_gettime(CLOCK_MONOTONIC)MSVC STL 用QueryPerformanceCounter两者精度不同不要拿某一家的分辨率去推断另一家。常见坑点1. 以为duration_cast会四舍五入// ❌ 1500ms 转秒得到 1而不是 2 auto s std::chrono::duration_caststd::chrono::seconds(std::chrono::milliseconds(1500)); // ✅ 需要四舍五入就用 roundC17 起 auto s2 std::chrono::roundstd::chrono::seconds(std::chrono::milliseconds(1500));2. 用 system_clock 测耗时// ❌ 两次 now() 之间可能被 NTP 校正差值可能为负或凭空变大 auto t0 std::chrono::system_clock::now(); // ✅ auto t0b std::chrono::steady_clock::now();3. 跨毫秒/秒边界时先转成裸整数// ❌ count() 之后单位就丢了后面再加就被当成同一单位 long long raw std::chrono::duration_caststd::chrono::seconds(d).count(); // ✅ 全程带着 duration 类型走只在输出/落库时才 count() std::chrono::seconds sec std::chrono::duration_caststd::chrono::seconds(d);4. 把浮点 Rep 混进整数运算链// ❌ double 型 duration 参与大数累加误差随规模增长 std::chrono::durationdouble acc(0); // ✅ 需要整数语义就用整数 Rep确实要浮点就用 long double 并明确误差预算 std::chrono::milliseconds acc2(0);5. 依赖 std::cout 直接打印 duration// ❌ C17 及以前没有为 duration 提供 operator std::cout std::chrono::milliseconds(5); // ✅ 打进 count()并自己带上单位 std::cout std::chrono::milliseconds(5).count() ms\n;C20 起才为 chrono 类型提供了operator具体可用性以你的标准库版本为准。6. 累计时间用秒级整数计数做除法// ❌ 每次累加都截断误差会累积变大1500ms 1500ms 两次转秒 2s而非 3s // ✅ 统一换算到最小单位再累加最后一次性换算 std::chrono::milliseconds total(0); total std::chrono::milliseconds(1500); total std::chrono::milliseconds(1500); // 3000ms最后再转秒得 3总结要点结论内部表示一个计数 std::ratio表示的单位单位是类型不是运行期值隐式转换判据目标 Rep 是浮点或源单位是目标单位整数倍且源 Rep 非浮点显式转换duration_cast向零截断需要取整方向用 C17 的floor/ceil/round公共类型Rep取common_typePeriod取分子 gcd、分母 lcm预定义别名具体整数类型实现定义libstdc 用longMSVC STL 用long long时钟测间隔用steady_clock墙上时间用system_clock溢出有符号 Rep 溢出是 UB必要时用更宽或浮点 Rep 并自行界定范围duration的设计思想其实只有一句话把单位和数值一起编码进类型让换算在编译期完成让精度损失必须被显式书写。理解了ratio_divide的分母是不是 1你就理解了它全部的类型规则理解了整数除法你就理解了它全部的取整行为。自己动手复现一遍之后再用标准库那些「为什么这里必须写 duration_cast」的疑问基本不会再出现。