CodeGym /Courses /C++ SELF /time_point and system_clock — “moment in time”

time_point and system_clock — “moment in time”

C++ SELF
Level 66, Lesson 1
Available

1. Why a “moment” is a separate type, not just a number

When you first hear “moment in time”, your brain suggests a lazy solution: “well, it's just a number of milliseconds, let's use long long”. And it even sometimes works. Until a second developer joins the project, a third storage format appears, and someone accidentally starts adding a “moment” to a “moment”, producing a mathematically meaningless value — like adding two street addresses and expecting to get a shop address.

In std::chrono a moment in time is modeled by the type time_point. The idea is simple: a time_point is a point on the time axis, bound to specific “clock” (Clock). Without the clock the moment is incomplete, because different clocks can have different origins and even different behavior.

You can remember a short rule that prevents half of the bugs:

Entity Question Type
Interval “How much time passed / how long to wait?”
std::chrono::duration
Moment “When did it happen / to which time is it attached?”
std::chrono::time_point

system_clock: “real time”, but with personality

std::chrono::system_clock is the clock that represents the system’s “current calendar time”. In other words, the “now” that a person sees on the screen, in logs, in file timestamps (in the general sense), in messages like “created 2 minutes ago”.

However, system_clock has an important human-like trait: the system can adjust the time. For example, network synchronization, manual user time changes, DST transitions and settings — all of these can affect the “calendar clock”. Therefore system_clock is ideal for “what to show to the user and what to record as a date in logs”, but not well suited for precise duration measurements. We will do precise measurements later with steady_clock; today we honestly focus on the “moment”.

From a technical perspective system_clock is a type that has a nested time_point type and a static now() method that returns the “moment now”.

2. time_point: obtaining and binding to an epoch

How to get a time_point

The first encounter with time_point usually looks like meeting something “templated and scary”. In fact, it can be very simple to use, especially if you use auto.

Here is a minimal basic example:


#include <chrono>
#include <iostream>

int main() {
    auto now = std::chrono::system_clock::now();

    (void)now; // just recorded for now: now is a time_point
    std::cout << "Got current time_point\n"; // Got current time_point
}

The key point here is that now is not a number and not even a duration. It is precisely a moment. It's useful not by itself (printing it “as is” is inconvenient), but as a building block for:

  1. storing a moment (for example, “when a task was created”),
  2. comparing moments (for example, “which happened earlier”),
  3. computing the interval between moments (time_point - time_point).

And yes: chrono is designed so that arithmetic and conversions are as type-safe as possible; the standard has been moving to allow duration/time_point operations to be constexpr where it makes sense.

Epoch and time_since_epoch()

Very often you need to store a moment “as a number”: for example, in a file, in a database, in JSON, or simply as a field in memory that is easy to sort and compare.

For this a time_point can be converted into a duration relative to some epoch — the origin chosen by the clock.

It is done like this:

#include <chrono>
#include <iostream>

int main() {
    auto now = std::chrono::system_clock::now();
    auto since_epoch = now.time_since_epoch(); // duration

    std::cout << "duration count (raw) = " << since_epoch.count() << '\n';
}

There is one caveat here, but a very common one: since_epoch.count() is the “raw number” in the units of the duration type used by system_clock. That type and its tick frequency depend on the implementation (platform/compiler). Therefore printing or storing count() without an explicit cast is almost always a bad idea.

The correct approach: cast the duration from the epoch to the desired unit. Usually milliseconds are chosen, sometimes seconds.

Example with milliseconds:

#include <chrono>
#include <iostream>

int main() {
    auto now = std::chrono::system_clock::now();

    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
        now.time_since_epoch()
    );

    long long ms_since_epoch = ms.count();
    std::cout << ms_since_epoch << " ms since epoch\n";
}

Here we do two important things. First, we explicitly specify the units (milliseconds). Second, we name the variable so that the units are included in the name (ms_since_epoch). This is not pedantry — it's prevention of bugs like “why is everything 1000 times larger”.

4. Arithmetic with time_point: what makes sense and what doesn't

When a “moment” appears in code, hands reach for familiar arithmetic. It's important to agree with yourself (and the compiler) beforehand.

The logic with time_point is:

  • time_point + duration yields a new time_point (the moment is shifted forward/backward).
  • time_point - time_point yields a duration (how much time between moments).
  • time_point + time_point is nonsense, and the standard library will not let you do it.

Let's look at a normal example: “5 seconds after the current moment”.

#include <chrono>
#include <iostream>

int main() {
    auto now = std::chrono::system_clock::now();
    auto later = now + std::chrono::seconds{5};

    auto diff = later - now; // duration
    auto s = std::chrono::duration_cast<std::chrono::seconds>(diff);

    std::cout << s.count() << " s\n"; // 5 s
}

Here is a very clear meaning: the difference of two moments is an interval. So we finally get “how much time passed” from “when it was”.

A special note for fans of “optimizing types”: sometimes you want to store time as an unsigned number (“it's non-negative!”). In practice this often produces unpleasant surprises when subtracting and comparing; there have even been separate bugs/defects in standard discussions related to subtracting time_point with unsigned duration.

In student code the rule is simple: if you are not 200% sure, do not turn time into unsigned. Store “milliseconds since epoch” in a normal signed integer type (long long or std::int64_t).

5. std::time_t: a bridge to the “old world”

Although std::chrono is the modern approach, the “old world” C time API is still around. You will encounter functions that take/return std::time_t. This is usually “seconds since epoch” (with second precision).

system_clock has ready-made functions for the bridge:

  • std::chrono::system_clock::to_time_t(time_point)
  • std::chrono::system_clock::from_time_t(time_t)

Minimal example:

#include <chrono>
#include <ctime>
#include <iostream>

int main() {
    auto now = std::chrono::system_clock::now();
    std::time_t t = std::chrono::system_clock::to_time_t(now);

    std::cout << t << " seconds since epoch\n";
}

It's important to understand the limitation: time_t usually stores seconds, so milliseconds will be “collapsed” when converting this way. For logs “in which second it happened” this is often enough, but for precise timestamps (for example, “button pressed at 12:34:56.789”) it's not.

6. Practical example: created_at and a task's “age”

To keep the topic from being abstract, let's develop a small console app that we continuously improve in the course. Let it be a simple “to-do list”: tasks store text and a creation moment. We are not saving anything to files or working with formats yet — today it's enough to learn how to stamp a timestamp and compare moments.

Let's make a task model that stores the creation moment as “milliseconds since epoch”. Why not store time_point directly in the struct? You can, but a number is easier to serialize and print, and it's not tied to a particular internal representation. We consciously fix the units: milliseconds.

#include <string>

struct Task {
    int id = 0;
    std::string text;
    long long created_ms = 0; // ms since epoch
};

Now let's write a function that gets “now” in milliseconds:

#include <chrono>

long long now_ms_since_epoch() {
    auto now = std::chrono::system_clock::now();
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
        now.time_since_epoch()
    );
    return ms.count();
}

Note: the function returns a number, but inside does everything “properly”: takes a time_point, converts it to a duration from the epoch, casts to milliseconds and only then extracts count().

Add creating a task:

#include <iostream>
#include <vector>

int main() {
    std::vector<Task> tasks;

    Task t;
    t.id = 1;
    t.text = "Buy milk (or compile errors)";
    t.created_ms = now_ms_since_epoch();

    tasks.push_back(t);
    std::cout << "Created task at " << t.created_ms << " ms\n";
}

The idea is that now each task has a “passport”: a creation moment. We can, for example, sort tasks by created_ms, compare, filter, find “the newest” — all without manual recalculations.

From number back to time_point

Sometimes you store a moment as a number (for example, created_ms), but you need to get a time_point again to do “proper chrono arithmetic” (for example, compare with system_clock::now()).

The scheme is:

  1. number of milliseconds → std::chrono::milliseconds{...}
  2. system_clock::time_point{milliseconds{...}}

Here is a small utility:

#include <chrono>

std::chrono::system_clock::time_point tp_from_ms(long long ms_since_epoch) {
    return std::chrono::system_clock::time_point{
        std::chrono::milliseconds{ms_since_epoch}
    };
}

And now we can compute a task's “age” in seconds without breaking types:

#include <chrono>

std::chrono::seconds task_age_seconds(const Task& t) {
    auto created_tp = tp_from_ms(t.created_ms);
    auto now = std::chrono::system_clock::now();

    auto age = now - created_tp; // duration
    return std::chrono::duration_cast<std::chrono::seconds>(age);
}

There is a subtle but important idea here: we don't “subtract milliseconds from milliseconds” as raw numbers, but work in chrono types. This is especially nice when units can change: today you print seconds, tomorrow minutes, and the logic remains correct.

7. Diagram: time_point, epoch and duration

Sometimes it's useful to see it once as a diagram and stop being afraid:

flowchart LR
    TP["time_point (moment)"] -->|"time_since_epoch()"| D["duration (interval from epoch)"]
    D -->|duration_cast<milliseconds>| MS["milliseconds"]
    MS -->|"count()"| N["integer number (ms since epoch)"]

The idea: time_point is convenient for moment arithmetic, duration is convenient for intervals, and a “number” is convenient for storage/transmission. You can go back and forth, but it's better to do it consciously and with fixed units.

8. Common mistakes when working with time_point and system_clock

Mistake #1: confusing time_point and duration (and trying to treat “intervals” as moments).
This mistake usually looks like: you store a “timeout 500ms” as system_clock::time_point, and then don't know what to do with it. A timeout is an interval; it belongs in duration. A moment (time_point) is needed when you know “when exactly”: “task creation time”, “last saved time”, “deadline”.

Mistake #2: using time_since_epoch().count() without duration_cast, then assuming it's milliseconds.
Classic trap: the code compiles, the number prints, and a month later you find out that on one machine it's “nanoseconds”, on another — “something else”. The right habit: always cast to an explicit unit (milliseconds, seconds) and only then call count().

Mistake #3: adding two time_points or “adding a number” without specifying units.
Adding two moments is like adding “Tuesday” and “Thursday”: linguistically possible, semantically meaningless. In chrono the normal operation is “moment + interval”. So if you want “in 5 seconds”, add std::chrono::seconds{5}, not + 5 (time doesn't like being treated as an int).

Mistake #4: storing a timestamp in unsigned and encountering oddities when subtracting.
It seems logical: “time is non-negative”. In practice subtraction and comparisons can start behaving unexpectedly, especially if a negative interval appears somewhere or different duration types are mixed. There have been subtle issues in the standard library and discussions around subtracting time_point with unsigned duration.
Use a signed type for “milliseconds since epoch” unless you have a very good reason not to.

Mistake #5: using system_clock to measure operation durations and being surprised by negative intervals.
This almost inevitably happens when the system clock is adjusted backwards while you're measuring “how long it took” using calendar time. Today we only mark this as a risk. The right tool for measurements is steady_clock, which will be covered in the next lecture.

1
Task
C++ SELF, level 66, lesson 1
Locked
Splitting the epoch
Splitting the epoch
1
Task
C++ SELF, level 66, lesson 1
Locked
Timestamp Duel
Timestamp Duel
1
Task
C++ SELF, level 66, lesson 1
Locked
Time shift
Time shift
1
Task
C++ SELF, level 66, lesson 1
Locked
Launch schedule
Launch schedule
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION