C++ 中的时间:时钟间转换、纪元和时长
Time in C++: Inter-Clock Conversions, Epochs, and Durations

原始链接: https://www.sandordargo.com/blog/2025/12/24/clocks-part-5-conversions

## C++ 中的时间处理:时钟、纪元和转换 C++ 的 `` 库提供了强大的时间处理功能,但使用不同的时钟并在它们之间进行转换需要仔细考虑。本文重点介绍了其中涉及的细微之处。 `` 中的每个时钟(如 `system_clock` 和 `steady_clock`)都有其自身的 *纪元*——零点。这意味着直接比较来自不同时钟的 `time_point` 值是没有意义的。除非存在明确的数学关系(如 C++20 的 `clock_cast` 和 `clock_time_conversion` 所定义),否则不建议在它们之间进行转换。 对于 *没有* 定义关系的时钟(如 `system_clock` 和 `steady_clock`),手动关联可以 *近似* 实现转换,但容易受到墙上时钟调整等问题的影响。 即使是时长也不简单;在单位之间进行转换可能导致截断或“虚假精度”。C++20 的 `floor`、`ceil` 和 `round` 函数有助于显式管理这些转换。在处理非常大的时长时,请注意潜在的整数溢出。 **关键要点:** 使用单个时钟(最好是 `steady_clock`)测量间隔,避免假设时钟纪元之间的关系,并明确舍入和精度。专注于验证你的 *逻辑*,而不是时钟本身,以获得健壮且可测试的代码。

这个Hacker News讨论围绕着使用``库在C++中进行时间管理的复杂性。一位用户分享了构建竞争性编程评判系统时的经验,发现依赖于关联`steady_clock`(用于时间限制)和`system_clock`(用于日志记录)在NTP同步期间导致问题——提交记录显示在代码修改*之前*发生。 解决方案,借鉴了链接文章中的建议,即*仅*使用`steady_clock`进行基于持续时间的强制执行,并显式捕获`system_clock::now()`以记录时间戳。 其他评论者强调了``的一些特点,包括在表示具有运行时确定转换为秒的钟(如CPU周期计数器)方面的局限性,以及在表示同时具有纳秒精度*和*广泛范围的日期方面的困难。他们还赞扬了C++20的`std::chrono::round`,它可以准确地对持续时间进行四舍五入,避免了静默的向下取整问题。
相关文章

原文

By now in this series, we’ve spent time looking at the major standard clocks and their behavior. We’ve talked about wall-clock time, monotonic clocks, and the myths around “high resolution”. Today, we are going to talk about a subtle area: how clocks relate to each other, how epochs differ, and what happens when you - need to - convert durations.

It sounds simple, right? A timestamp is a timestamp, and a duration is just a number of seconds - or other time units. But <chrono> is designed to be type-safe, and it enforces some rules that prevent accidental misuse. Once you understand why those rules exist, time handling in C++ starts to feel much safer — and your tests get more reliable too. And

It sounds simple, right? A timestamp is a timestamp, and a duration is just a number of seconds - or other time units. But when it comes to converting between clocks, the reality is much messier. Different clocks have different epochs, different guarantees, and sometimes completely different purposes. <chrono> doesn’t stop you from doing conversions just to be pedantic — it discourages them because many of those conversions simply don’t make sense or can silently introduce subtle bugs.

Once you understand why these conversions are tricky, and why the library forces you to be explicit about them, the design choices in <chrono> click into place. And more importantly, you start avoiding whole classes of timing bugs — both in production and in tests.

Let’s jump right into the details.

Clock Epochs: Why “Zero” Isn’t Universal

A time_point in <chrono> is always measured relative to some epoch. But here’s the catch: each clock defines its own epoch.

  • std::chrono::system_clock uses the Unix epoch (1 January 1970 UTC).
  • std::chrono::steady_clock uses an unspecified monotonic epoch. Its zero might be system boot time, or something entirely different.
  • std::chrono::high_resolution_clock is usually just an alias to either system_clock or steady_clock.

This means:

You cannot meaningfully compare time_points from different clocks.

If you try to subtract a steady_clock::time_point from a system_clock::time_point, you’re effectively asking: “What is the difference between 1970-01-01 and some arbitrary boot-time counter?”

The answer, of course, is: it depends on the machine, the OS, and maybe even the phase of the moon… :)

Even tests can fall into this trap. If a test assumes that steady_clock starts at zero or that its epoch is stable across runs or platforms, that test becomes brittle. When you need deterministic behavior, it’s best to use controllable test clocks — or simply avoid exposing epochs at all.

Converting Between Clocks: What You Can (and Can’t) Do

Converting between clocks is tricky because their epochs differ—sometimes radically.
For years, the standard library offered no built-in mechanism to transform a time_point from one clock into another. That changed with C++20: we now have clock_cast and custom clock_time_conversion specializations, which allow well-defined conversions when clocks have a meaningful relationship.

At the same time, many clocks (such as system_clock and steady_clock) still cannot be safely converted because they measure fundamentally different notions of time. For those cases, you must fall back to a manual correlation technique, understanding that the result is only an approximation.

Let’s look at both.

Standard-Supported Conversions (clock_cast and clock_time_conversion)

C++20 introduced std::chrono::clock_cast, which allows converting a time_point from one clock to another when the conversion is defined.

A conversion is defined if:

  1. The clocks have a known, stable mathematical relationship
  2. A clock_time_conversion<FromClock, ToClock> specialization exists

The standard library already provides such conversions between the following pairs:

  • system_clock
  • utc_clock
  • tai_clock
  • gps_clock
  • file_clock
  • Custom clocks if clock_time_conversion is soecified

These clocks share known epochs and offsets (e.g., TAI is always 37 seconds ahead of UTC at the moment of writing), so the library can safely compute conversions.

More on some C++20 clocks next week.

Example: converting utc_clock to tai_clock:

1
2
3
4
using namespace std::chrono;

auto utc_now = utc_clock::now();
auto tai_now = clock_cast<tai_clock>(utc_now);

Here the result is well-defined and stable, because the relationship between TAI and UTC is part of the standard, not dependent on your system’s wall clock or boot time.

You can also define your own conversions by providing a clock_time_conversion specialization for your custom clocks. This is particularly useful for:

  • virtual/test clocks
  • simulated clocks
  • domain-specific clocks (e.g., frame counters, monotonic-but-shifted clocks)

As soon as the specialization exists, clock_cast becomes available.

Manual Correlation (When No Meaningful Conversion Exists)

For clocks that don’t have a fixed mathematical relationship — like system_clock and steady_clock — the standard cannot give you a correct conversion. Their epochs differ and their behavior differs; one jumps, one doesn’t.

In these cases, you can only estimate a conversion using a manual correlation pattern:

1
2
3
4
5
auto system_now = std::chrono::system_clock::now();
auto steady_now = std::chrono::steady_clock::now();

// The offset lets you map steady to system later on.
auto offset = system_now - steady_now;

From here, you can convert from steady time to approximate system time:

1
auto estimated_system_time = some_steady_tp + offset;

This is useful, for example, when you measure an event duration with steady_clock (which is good for accuracy), but still want to log a human-readable timestamps.

However, there’s a big caveat! If system_clock jumps (e.g. due to NTP sync or to manual clock change), your offset becomes invalid.

If you rely on this relationship for long-running processes, you’re essentially betting that the wall clock won’t move. That’s a dangerous bet.

Testing-wise, this is exactly the kind of logic that benefits from dependency injection: give the code two controllable clocks and make the conversion behavior explicit. Tests shouldn’t rely on the real-world relationship between clocks; they should verify your conversion math.

Duration Casting and Precision

Durations seem simple — just a number plus a unit. But converting between units introduces subtle precision issues.

On the one hand, casting to a coarser unit truncates:

1
2
3
4
using namespace std::chrono_literals;
auto ns = 1500ns;            // 1500 nanoseconds
auto us = std::chrono::duration_cast<std::chrono::microseconds>(ns);
// us == 1 microsecond (the remaining 500ns are lost)

On the other hand, casting to a finer unit introduces “imaginary precision”:

1
2
3
std::chrono::milliseconds ms{1};
auto ns2 = std::chrono::duration_cast<std::chrono::nanoseconds>(ms);
// ns2 == 1'000'000ns, but we didn't *measure* at nanosecond precision

C++20 gives us chrono::floor, ceil, and round, which make intent clear and help you see where you’re losing information.

We are explicit in this lossy cast: we choose to lose 499ns, and that’s fine as long as it’s intentional.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// https://godbolt.org/z/K7GPdKMn9
using namespace std::chrono_literals;
auto original = 1499ns;

// Round to the *nearest* microsecond
auto rounded_us = std::chrono::round<std::chrono::microseconds>(original);
// rounded_us == 1us

// We've effectively decided that 1499ns ≈ 1µs
// The remaining 499ns are gone by design.
std::cout << original.count() << "ns\n";
std::cout << rounded_us.count() << "us\n";
/*
1499ns
1us
*/

Overflow, Underflow, and Representation Limits

Another subtle danger lies in subtraction or conversion involving very large durations.

Imagine you accidentally subtract two system_clock time_points taken decades apart, or you add a huge duration that exceeds 64-bit limits. Normally, modern platforms give you plenty of room, but it’s not infinite.

As there is almost always an integer type behind std::chrono::duration<Rep, Period>’s Rep, there is a risk of signed integer overflow and therefore undefined behaviour.

Yet, it’s useful to use signed integer types as Rep, because negative durations can happen legitimetly or they can show a logic error that would be otherwise hard to spot.

Conclusion

Inter-clock conversions turn out to be one of the trickiest parts of working with <chrono>. Clocks have different epochs, some jump while others don’t, and durations behave differently depending on how you convert them. But with the right mental model — and a few best practices — you can write robust, portable, and testable time-handling code.

  • Always measure intervals using one clock, preferably use steady_clock
  • Convert to human-readable preresentations only at the boundary
  • Never assume epochs are related. Even if two timestamps “look close”, you can’t rely on any stable relationship between clocks.
  • Keep rounding and precision choices explicit. Use floor, ceil or round to do so.
  • Don’t validate clocks, validate your logic.

Get those right, and suddenly time in C++ becomes a lot less mysterious.

Next week, we’ll talk about some additional clocks introduced by C++20. Stay tuned!

Connect deeper

If you liked this article, please

联系我们 contact @ memedata.com