Skip to content
C++
Library
since C++20
Intermediate

std::chrono — Advanced Usage

Advanced C++ chrono covering C++20 calendar types, time zones, zoned_time, date arithmetic, DST edge cases, formatting, and clock selection.

std::chrono (C++20 calendar and timezone extensions)since C++20

C++20 added a civil calendar system (year_month_day, weekday, month_day_last, …), an IANA timezone database (get_tzdb()), and zoned_time for representing wall-clock instants in a specific geographic zone — all formattable via std::format.

Overview

The chrono library arrived in C++11 with clocks, durations, and time points. C++20 made a sweeping extension: a full civil calendar and a timezone database derived from the IANA TZDB. These are separate abstractions that compose:

  • Calendar types (year_month_day, weekday, year_month_weekday, …) represent civil dates with no epoch or clock.
  • sys_days / local_days are time_point specialisations with day resolution that bridge the calendar to the clock.
  • zoned_time<Duration> pairs a const time_zone* with a sys_time to represent a wall-clock moment in a geographic zone.

std::print / std::println used in examples below require C++23 (<print>). For C++20, substitute std::cout << std::format(...).


Calendar Types

cpp
#include <chrono>
using namespace std::chrono;
using namespace std::chrono_literals;  // enables 2025y, 15d, 9h, 30min, etc.

// ── Constructing dates ─────────────────────────────────────────────────────
year_month_day d1 = 2025y / January / 15d;  // operator/ builds from right
year_month_day d2 = 2025y / 1       / 15;   // integer month and day accepted
year_month_day d3 = 15d  / January  / 2025y; // day/month/year order also valid

// Today's UTC date
year_month_day today = floor<days>(system_clock::now());  // C++20

// ── Accessors ─────────────────────────────────────────────────────────────
// year/month/day are strong typedefs — cast to get numeric values
int      y_int = static_cast<int>(today.year());
unsigned m_num = static_cast<unsigned>(today.month()); // 1–12
unsigned d_num = static_cast<unsigned>(today.day());   // 1–31

// ── Day arithmetic — go through sys_days to avoid invalid intermediates ────
sys_days as_days   = today;                          // implicit conversion
sys_days next_week = as_days + days{7};
year_month_day nwd = next_week;                      // back to calendar

// Month/year arithmetic operates directly on calendar components (can produce invalid dates)
year_month_day next_month = today + months{1};
year_month_day last_year  = today - years{1};

// ── End-of-month helpers ──────────────────────────────────────────────────
year_month_day_last eom   = today.year() / today.month() / last; // C++20
year_month_day      eom_d = eom;           // converts to a concrete day

// ── Weekday ───────────────────────────────────────────────────────────────
weekday dow = weekday{sys_days{today}};
bool is_mon = (dow == Monday);
unsigned c_enc  = dow.c_encoding();    // 0=Sun … 6=Sat
unsigned iso_enc = dow.iso_encoding(); // 1=Mon … 7=Sun

// ── Nth-weekday-of-month ──────────────────────────────────────────────────
// Third Monday of March 2025
year_month_weekday ymw = 2025y / March / Monday[3]; // C++20
year_month_day     ymd = sys_days{ymw};             // resolve to a concrete date

// Last Monday of March 2025
year_month_weekday_last ymwl = 2025y / March / Monday[last]; // C++20
year_month_day          ymd2 = sys_days{ymwl};

// ── Time-of-day decomposition (hh_mm_ss, C++20) ──────────────────────────
auto now       = system_clock::now();
auto since_mid = now - floor<days>(now);     // duration since midnight UTC
hh_mm_ss tod{floor<seconds>(since_mid)};
tod.hours();    // hours since midnight
tod.minutes();  // [0, 60)
tod.seconds();  // [0, 60)

Date Validity and Normalisation

Calendar types can silently represent invalid dates; no exception is thrown.

cpp
year_month_day invalid = 2025y / February / 30d;
invalid.ok();  // false — no exception raised

// Normalise by round-tripping through sys_days
year_month_day normalised{sys_days{invalid}};
// 2025-Feb-30 → 2025-Mar-02 (2025 is not a leap year)

// Classic end-of-month trap: Jan 31 + 1 month = Feb 31 (invalid)
year_month_day jan31   = 2025y / January / 31d;
year_month_day feb_bad = jan31 + months{1};   // ok() == false!
// Clamp to last valid day in the target month:
year_month_day feb_clamped{sys_days{feb_bad}}; // 2025-Mar-03 — not Feb 28!
// If you want Feb 28, use year_month_day_last as the intermediate:
year_month_day feb_last = (jan31.year() / (jan31.month() + months{1}) / last);
// → 2025-Feb-28

// Leap-year check
bool is_leap = year{2024}.is_leap(); // true

When adding months or years, always call ok() afterwards and normalise through sys_days or clamp via year_month_day_last.


Time Zones

C++20 provides an IANA timezone database. On Linux the database is read from /usr/share/zoneinfo (requires tzdata package). MSVC bundles its own copy. On embedded targets the database may be absent.

cpp
#include <chrono>
using namespace std::chrono;

// ── Lookup and enumerate ──────────────────────────────────────────────────
const tzdb&      db   = get_tzdb();
const time_zone* ny   = locate_zone("America/New_York"); // throws if not found
const time_zone* cur  = current_zone();                  // local system zone

// ── UTC → zoned time ──────────────────────────────────────────────────────
auto utc_now = system_clock::now();              // sys_time<nanoseconds>
zoned_time<milliseconds> zt_ny{ny, floor<milliseconds>(utc_now)};

// Zone-to-zone: pass another zoned_time as source
zoned_time<milliseconds> zt_tok{"Asia/Tokyo", zt_ny};

// Inspect the UTC offset and abbreviation at this instant
sys_info info = zt_ny.get_info();
// info.offset  — std::chrono::seconds (e.g. -18000s = -5h)
// info.abbrev  — std::string (e.g. "EST")
// info.save    — std::chrono::minutes (DST saving: 0 or 60)

// ── Local date+time → zoned time ─────────────────────────────────────────
// local_days{ymd} uses year_month_day::operator local_days() (explicit, C++20)
auto zt_chi = zoned_time{
    "America/Chicago",
    local_days{2025y / March / 15d} + 9h + 30min  // treated as Chicago local time
};

// ── DST gaps and overlaps ─────────────────────────────────────────────────
// Spring-forward gap: 2025-03-09 02:00–03:00 does not exist in America/New_York
try {
    zoned_time zt_gap{
        "America/New_York",
        local_days{2025y / March / 9d} + 2h + 30min
    };
} catch (const nonexistent_local_time& e) {
    // local time falls in the skipped hour
}

// Fall-back overlap: 2025-11-02 01:30 exists twice in America/New_York
try {
    zoned_time zt_amb{
        "America/New_York",
        local_days{2025y / November / 2d} + 1h + 30min
    };
} catch (const ambiguous_local_time& e) {
    // resolve with choose::earliest or choose::latest
}

// Silent resolution using choose::
zoned_time zt_resolved{
    "America/New_York",
    local_days{2025y / November / 2d} + 1h + 30min,
    choose::latest  // take the post-fallback (standard time) occurrence
};

system_clock always tracks UTC — C++20 made this normative. local_time<D> is a type-level tag marking a time point as "local to some unspecified zone"; it only becomes meaningful when paired with a time_zone* via zoned_time.


Formatting and Parsing

std::format (C++20) understands all chrono types natively.

cpp
using namespace std::chrono;

auto now   = system_clock::now();
auto today = floor<days>(now);

// Date
std::println("{:%Y-%m-%d}", today);             // C++23 println; format is C++20
// → 2025-03-15
std::println("{:%A, %B %d, %Y}", today);
// → Saturday, March 15, 2025

// Time with configurable precision
std::println("{:%H:%M:%S}", floor<milliseconds>(now));
// → 14:32:07.483
std::println("{:%H:%M:%S}", floor<seconds>(now));
// → 14:32:07

// Zoned time (zone abbreviation via %Z, UTC offset via %z)
zoned_time zt{"Europe/Paris", now};
std::println("{:%Y-%m-%d %H:%M %Z}", zt);      // 2025-03-15 15:32 CET
std::println("{:%Y-%m-%d %H:%M %z}", zt);      // 2025-03-15 15:32 +0100

// Parsing (C++20) — std::chrono::parse as stream extractor
{
    std::istringstream ss{"2025-03-15 14:32:07"};
    sys_seconds tp;
    ss >> parse("%Y-%m-%d %H:%M:%S", tp);
}

// Parse with UTC offset — %Ez accepts ±hh:mm or ±hhmm
{
    std::istringstream ss{"2025-03-15 09:32:07 -05:00"};
    sys_seconds tp;
    ss >> parse("%F %T %Ez", tp);  // absorbs offset, result is UTC
}

// Parse with timezone name into a separate string
{
    std::istringstream ss{"1999-10-31 01:30:00 -08:00 US/Pacific"};
    sys_seconds tp;
    std::string tz_name;
    ss >> parse("%F %T %Ez %Z", tp, tz_name);
}

Clock Selection

cpp
// steady_clock — monotonic, never adjusted; the right choice for elapsed time
// C++11
auto t0 = steady_clock::now();
do_work();
auto elapsed = duration_cast<milliseconds>(steady_clock::now() - t0);

// system_clock — Unix epoch (UTC, normative in C++20); use for wall-clock timestamps
// C++11; to_time_t/from_time_t for C interop
auto wall = system_clock::now();
time_t tt = system_clock::to_time_t(wall);

// high_resolution_clock — implementation-defined; may alias system_clock on some
// platforms (not guaranteed monotonic). Prefer steady_clock for benchmarks.
// C++11 — avoid in new code.

// utc_clock — like system_clock but counts leap seconds; epoch = 1970-01-01 UTC
// C++20
auto utc = utc_clock::now();
sys_time<seconds> sys = utc_clock::to_sys(floor<seconds>(utc));

// tai_clock — International Atomic Time; ahead of UTC by 10s + accumulated leap seconds
// C++20
auto tai = tai_clock::now();

// gps_clock — GPS epoch 1980-01-06 00:00:00 UTC; no leap seconds
// C++20
auto gps = gps_clock::now();

// file_clock — clock used by std::filesystem::file_time_type; epoch unspecified
// C++20
auto ft = file_clock::now();

// clock_cast (C++20) — converts between any two standard clocks
auto sys_ft  = clock_cast<system_clock>(ft);
auto utc_tai = clock_cast<tai_clock>(utc);

Avoid manual epoch arithmetic between clocks. clock_cast handles the leap-second offset between utc_clock and system_clock correctly; hand-rolling it is error-prone and breaks on new leap seconds.


Duration Arithmetic

cpp
using namespace std::chrono_literals;

// Narrowing is rejected at compile time — no silent truncation
// duration<long long, milli> ms = seconds{5}; // ERROR

// Explicit cast — truncates toward zero
auto ms = duration_cast<milliseconds>(5s);  // 5000ms

// Widening is implicit (floating-point representation)
duration<double> sec_f = 1500ms;            // 1.5s

// floor / ceil / round (C++17) — follow mathematical rounding
auto t = 1'500ms;
floor<seconds>(t);  // 1s  — toward -∞
ceil<seconds>(t);   // 2s  — toward +∞
round<seconds>(t);  // 2s  — ties to even

// For negative durations, floor and duration_cast differ:
auto neg = -1'500ms;
duration_cast<seconds>(neg);  // -1s  (toward zero)
floor<seconds>(neg);           // -2s  (toward -∞)

// abs (C++17) — result type matches input
auto pos = abs(neg);  // 1500ms

// Mixed-unit arithmetic — result is the GCD duration
auto total = 2h + 30min + 45s;
std::println("{}s", duration_cast<seconds>(total).count()); // 9045

Benchmarking Helper

cpp
template <typename Duration = std::chrono::milliseconds, typename F>
auto benchmark(F fn, int iters = 1) -> Duration {
    using namespace std::chrono;
    auto t0 = steady_clock::now();
    for (int i = 0; i < iters; ++i) fn();
    return duration_cast<Duration>((steady_clock::now() - t0) / iters);
}

auto avg_ms = benchmark([&] { sort_large_vec(); }, 200);
std::println("avg: {}ms", avg_ms.count()); // C++23

Divide the raw duration before casting to preserve precision — (t1 - t0) / iters divides the nanoseconds integer before the cast, avoiding catastrophic truncation when iters is large.


Best Practices

  • Round before formatting: floor<seconds>(system_clock::now()) eliminates nanosecond noise in logs without sacrificing the precision you care about.
  • Use sys_days as the day-arithmetic bus: Never hold a year_month_day across a months{} or years{} addition without immediately calling ok(). Normalise through sys_days if you want overflow to carry into the next month, or use year_month_day_last if you want end-of-month clamping.
  • Prefer steady_clock for elapsed time: system_clock can jump backward due to NTP, leap-second smearing, or manual adjustment.
  • Always handle DST exceptions when constructing zoned_time from local time: Wrap in try/catch for nonexistent_local_time and ambiguous_local_time, or pass choose::earliest/choose::latest to resolve silently with documented policy.
  • Use clock_cast for cross-clock conversion: It handles the leap-second difference between system_clock and utc_clock automatically.
  • Check timezone database availability at startup: On Linux, if tzdata is missing, get_tzdb() throws std::runtime_error. Use locate_zone() instead of constructing zoned_time from a raw string to detect missing zones before use.

Common Pitfalls

Month-end arithmetic silently produces invalid dates:

cpp
auto d    = 2025y / January / 31d;
auto next = d + months{1};  // year_month_day for 2025-Feb-31 — ok() == false
// No exception is thrown. Always check ok() after months{}/years{} arithmetic.

high_resolution_clock is not guaranteed monotonic: On some implementations (notably older MSVC) high_resolution_clock aliased system_clock, which can go backward. steady_clock is the only guaranteed monotonic clock.

duration_cast truncates toward zero, not toward negative infinity:

cpp
duration_cast<seconds>(-1'500ms);  // -1s  (not -2s)
floor<seconds>(-1'500ms);          // -2s

Use floor when the result feeds into calendar arithmetic where negative rounding matters.

local_days is not UTC days:

cpp
// local_days marks a time_point as "local but timezone unspecified"
// It is NOT interchangeable with sys_days — they have different clock types.
sys_days utc_d = sys_days{2025y / March / 15d};    // explicit: UTC date
local_days loc_d = local_days{2025y / March / 15d}; // explicit: local date (no zone)
// Assigning one to the other is a compile error — the type system enforces this.

Parsing absorbs the UTC offset but does not infer a timezone: ss >> parse("%F %T %Ez", tp) adjusts tp to UTC using the parsed offset. The timezone abbreviation (%Z) if present is stored separately and is informational only — it does not look up the IANA database.


See Also