Skip to content

A time point knows its clock

Level: 201 · working knowledge

One line: system_clock::now() - steady_clock::now() does not compile, because a time_point carries its clock in its type — and the refusal is the point: the two clocks count from different moments, so their difference would be a number with a unit and no meaning.

#include <chrono>

auto start = std::chrono::steady_clock::now();
// ... the work ...
auto elapsed = std::chrono::steady_clock::now() - start;   // two points on ONE clock: a duration

The slide

The talk's Type safety slide (slide source ↗) shows three lines. The slide hides two more — the #include, and the line that makes sc mean std::chrono — so here is the whole file, with the third line commented out:

#include <chrono>
namespace sc = std::chrono;             // the line the slide hides

int main() {
    auto t1 = sc::steady_clock::now();  // a time_point on the steady clock
    auto t2 = sc::system_clock::now();  // a time_point on the system clock
    // auto diff = t2 - t1;
}

Uncomment line 7 — auto diff = t2 - t1; — and GCC refuses:

GCC 13.4, the Ubuntu CI runner's major version — g++ -std=c++20 -c slide.cpp — abridged: the first 7 of 51 lines
slide.cpp: In function 'int main()':
slide.cpp:7:20: error: no match for 'operator-' (operand types are 'std::chrono::time_point<std::chrono::_V2::system_clock, std::chrono::duration<long int, std::ratio<1, 1000000000> > >' and 'std::chrono::time_point<std::chrono::_V2::steady_clock, std::chrono::duration<long int, std::ratio<1, 1000000000> > >')
    7 |     auto diff = t2 - t1;
      |                 ~~ ^ ~~
      |                 |    |
      |                 |    time_point<std::chrono::_V2::steady_clock,[...]>
      |                 time_point<std::chrono::_V2::system_clock,[...]>

Reading the error

  • no match for 'operator-' — GCC looked for a - that accepts these two operand types and found none.
  • The two operand types. The left operand, t2, is a time_point<system_clock, duration<long, nano>>; the right, t1, is a time_point<steady_clock, duration<long, nano>>. Under the ~~ ^ ~~, GCC names each operand again and writes [...] for the second template argument, because it is the same on both sides — so the clock is the only difference left on the screen. The slide trims even the [...].
  • _V2 is an inline namespace libstdc++ puts its clocks in, so that a later version can redefine them while old binaries keep the old definitions — its source comment says the wrapping gives the current clocks unique mangled names (bits/chrono.h ↗). Read std::chrono::_V2::steady_clock as std::chrono::steady_clock; Clang hides inline namespaces and prints it that way.
  • Why there is no match. The standard declares the subtraction of two time points with one Clock parameter for both operands (time.point.nonmember ↗):
template<class Clock, class Duration1, class Duration2>
  constexpr common_type_t<Duration1, Duration2>
    operator-(const time_point<Clock, Duration1>& lhs, const time_point<Clock, Duration2>& rhs);

The durations may differ — that is why there are two of them — but there is only one clock. Deduction gets system_clock from the left operand and steady_clock from the right, cannot make Clock both, and discards the candidate. The rest of GCC's 51 lines are other operator-s — for reverse_iterator, move_iterator and so on — failing for the same reason.

Why refusing is right

The two clocks count from different moments. system_clock measures "time since 1970-01-01 00:00:00 UTC excluding leap seconds" (time.clock.system ↗); steady_clock's epoch is unspecified. You can see what the refused subtraction would have meant by removing the clocks first — time_since_epoch() returns a bare duration, and bare durations subtract whatever clock they came from:

auto nonsense = t2.time_since_epoch() - t1.time_since_epoch();          // compiles
std::cout << std::chrono::duration_cast<std::chrono::days>(nonsense);   // 20699d on this Mac
Measured 2026-09-10 — the two lines above — summary — not an answer key
Apple clang 21 / libc++ 21, this Mac       20699d
GCC 13.4 / libstdc++, Docker's Linux VM    20702d

The number is not random, which is what makes it instructive. Time since 1970 minus time since boot is the moment the machine booted, counted in days from 1970: day 20,699 is 2026-09-03, and sysctl kern.boottime says this Mac booted on 3 September 2026 at 17:31. Docker's VM booted three days later. As a duration, it means nothing — and computing it is exactly what the type refused to let you do by accident.

What compiles and what does not

Verified output of a_time_point_knows_its_clock.cpp — regenerated by tools/run_examples.py and held to the same answer key on libstdc++ and libc++ in CI, never hand-typed.

steady - steady  compiles: true
system - system  compiles: true
system - steady  compiles: false
system < steady  compiles: false
steady + steady  compiles: false
steady + seconds compiles: true
later - start = 2000000000ns
in seconds    = 2s
system.time_since_epoch() - steady.time_since_epoch() compiles: true

subtractable<A, B> is a C++20 concept that asks the compiler whether a - b would compile and answers true or false instead of stopping the build, so the slide's refusal becomes a line of output — the same line on both standard libraries. The six questions are the rules of time-point arithmetic:

Expression Compiles? Gives
steady - steady yes a duration: how far apart
system - system yes a duration, which may be negative
system - steady no — different clocks
system < steady no — comparison has one Clock parameter too
steady + steady no — adding two moments means nothing, even on one clock
steady + seconds yes a time point: a moment, moved

The last line of the output is the hole in the fence. time_since_epoch() — like .count() on a duration — hands back something with no clock attached, and from there the compiler cannot help you.

When you need both clocks

Take both readings, at the same moment, and never subtract across them: measure with the steady_clock pair, and use the system_clock reading only to say when it happened. C++20's clock_cast converts between clocks that have a defined relationship — system_clock, utc_clock, tai_clock, gps_clock and file_clock — and not steady_clock, which has none:

Measured 2026-09-10 — clock_cast<system_clock>(steady_clock::now()) — summary — not an answer key
GCC 13.4, 14.4, 15.2   error: no matching function for call to 'clock_cast<std::chrono::_V2::system_clock>(std::chrono::_V2::steady_clock::time_point)'
Apple clang 21         error: no member named 'clock_cast' in namespace 'std::chrono'

The second line is a different fact: libc++ 21 does not have clock_cast at all yet.

Clang says it differently

Apple clang 21 / libc++ 21 — clang++ -std=c++20 -c slide.cpp — abridged: the first 3 of 82 lines
slide.cpp:7:20: error: invalid operands to binary expression ('time_point' (aka 'time_point<system_clock>') and 'time_point' (aka 'time_point<std::chrono::steady_clock, duration<long long, ratio<1LL, 1000000000LL>>>'))
    7 |     auto diff = t2 - t1;
      |                 ~~ ^ ~~

The same refusal in other words — invalid operands for no match — followed by 26 notes. The left type is printed as plain time_point<system_clock> because on libc++ that clock's time point uses its default duration, microseconds, and Clang leaves default template arguments out.

If you are coming from another language

  • Rust. The same refusal from a different mechanism: Instant and SystemTime are unrelated types, and SystemTime - Instant is error E0308. Rust also refuses the subtraction C++ allows, SystemTime - SystemTime, because the wall clock can go backwards and a Rust Duration cannot be negative: An Instant is not a SystemTime ↗.
  • Python. Every clock returns a float, so nothing is refused: time.time() - time.monotonic() runs, and on this Mac with Python 3.14.7 it printed 1788673816.04 — the same boot-moment nonsense, in seconds. It lands on 6 September rather than 3 September because Python's monotonic() stops while the machine sleeps (Three clocks).
  • ABAP. (Not machine-checked.) GET RUN TIME FIELD and GET TIME STAMP FIELD both hand back plain numbers, so subtracting one from the other compiles — the mixed-clock subtraction with nothing to stop it.

See also