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++20C++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_daysaretime_pointspecialisations with day resolution that bridge the calendar to the clock.zoned_time<Duration>pairs aconst time_zone*with asys_timeto 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
#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.
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(); // trueWhen 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.
#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.
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
// 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
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()); // 9045Benchmarking Helper
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++23Divide 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_daysas the day-arithmetic bus: Never hold ayear_month_dayacross amonths{}oryears{}addition without immediately callingok(). Normalise throughsys_daysif you want overflow to carry into the next month, or useyear_month_day_lastif you want end-of-month clamping. - Prefer
steady_clockfor elapsed time:system_clockcan jump backward due to NTP, leap-second smearing, or manual adjustment. - Always handle DST exceptions when constructing
zoned_timefrom local time: Wrap intry/catchfornonexistent_local_timeandambiguous_local_time, or passchoose::earliest/choose::latestto resolve silently with documented policy. - Use
clock_castfor cross-clock conversion: It handles the leap-second difference betweensystem_clockandutc_clockautomatically. - Check timezone database availability at startup: On Linux, if
tzdatais missing,get_tzdb()throwsstd::runtime_error. Uselocate_zone()instead of constructingzoned_timefrom a raw string to detect missing zones before use.
Common Pitfalls
Month-end arithmetic silently produces invalid dates:
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:
duration_cast<seconds>(-1'500ms); // -1s (not -2s)
floor<seconds>(-1'500ms); // -2sUse floor when the result feeds into calendar arithmetic where negative rounding matters.
local_days is not UTC days:
// 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.
Parsing Dates from Strings
std::chrono::parse is the standard way to deserialise a date/time string in C++20. It is a stream extractor, not a free function returning a value, so you feed it an std::istringstream and let it fill a typed time_point or duration. The format string accepts the same tokens as std::format (and POSIX strftime), so %F is shorthand for %Y-%m-%d and %T expands to %H:%M:%S.
// C++20
#include <chrono>
#include <sstream>
using namespace std::chrono;
// ── Basic date+time parse ─────────────────────────────────────────────────
{
std::istringstream ss{"2025-11-03 09:15:00"};
sys_seconds tp;
ss >> parse("%F %T", tp);
// tp represents 2025-11-03T09:15:00 UTC
if (ss.fail()) { /* format mismatch or overflow */ }
}
// ── Parse with UTC offset — result is always converted to UTC ────────────
{
std::istringstream ss{"2025-11-03 09:15:00 -05:00"};
sys_seconds tp;
ss >> parse("%F %T %Ez", tp);
// tp == 2025-11-03T14:15:00 UTC (offset absorbed, then discarded)
}
// ── Parse into a calendar type via sys_days ───────────────────────────────
{
std::istringstream ss{"2025-11-03"};
sys_days dp;
ss >> parse("%F", dp);
year_month_day ymd{dp}; // 2025-Nov-03 as a calendar value
}
// ── Capture the abbreviation string separately ────────────────────────────
{
std::istringstream ss{"2025-03-09 03:45:00 -04:00 EDT"};
sys_seconds tp;
std::string abbrev;
// parse(fmt, tp, &abbrev) — third arg is optional std::string*
ss >> parse("%F %T %Ez %Z", tp, abbrev);
// abbrev == "EDT"; tp is UTC; no IANA lookup is performed
}
// ── Parse a bare duration ─────────────────────────────────────────────────
{
std::istringstream ss{"125"};
minutes dur;
ss >> parse("%M", dur); // dur == 125min (does not wrap at 60)
}Error handling requires an explicit ss.fail() check after the extraction — parse sets the stream's failbit on a format mismatch or when the parsed value cannot be represented in the target type. There is no exception thrown by default. If the input string may contain timezones by name (e.g., "America/New_York"), you must capture the name via the optional string argument and then call locate_zone(abbrev) yourself; parse never performs an IANA database lookup.
Duration Arithmetic Edge Cases
Integer overflow in std::chrono::duration follows the same rules as the underlying rep type — there is no saturation or exception. The default representation for nanoseconds is long long, which overflows after roughly 292 years. Mixing large multipliers with fine-grained durations is the most common trap.
// C++20
#include <chrono>
using namespace std::chrono;
using namespace std::chrono_literals;
// ── Overflow example — nanoseconds rep is long long (~292 yr max) ─────────
auto big = nanoseconds::max(); // 9'223'372'036'854'775'807 ns ≈ 292 y
auto overflowed = big + nanoseconds{1}; // undefined behaviour — wraps silently
// Use a wider rep or a coarser duration to avoid overflow:
using ns128 = duration<__int128, std::nano>; // GCC/Clang extension
using days64 = duration<long long, std::ratio<86400>>;
auto safe = duration_cast<days64>(big); // 106'751 days — fits in long long
// ── duration_cast truncates toward zero ──────────────────────────────────
duration_cast<seconds>( 1'999ms); // 1s (drops 999ms)
duration_cast<seconds>(-1'999ms); // -1s (drops 999ms — toward zero, not -∞)
// ── C++17 floor / ceil / round — mathematical rounding ───────────────────
floor<seconds>( 1'999ms); // 1s (toward -∞, same as cast for positive)
floor<seconds>(-1'999ms); // -2s (toward -∞ — differs from cast!)
ceil<seconds>( 1'001ms); // 2s (toward +∞)
ceil<seconds>(-1'001ms); // -1s (toward +∞)
round<seconds>( 1'500ms); // 2s (ties to even: 2 is even)
round<seconds>( 2'500ms); // 2s (ties to even: 2 is even)
round<seconds>( 1'499ms); // 1s
// ── Choosing the right rounding for calendar work ─────────────────────────
// floor is correct when computing "which day does this UTC instant fall on":
auto now = system_clock::now();
sys_days day = floor<days>(now); // always rounds back to midnight UTC
// ceil is useful for "how many whole seconds must I wait?":
auto wait_ns = 1'200'000'000ns;
auto wait_sec = ceil<seconds>(wait_ns); // 2s — conservative, never under-waits
// round is appropriate for display (e.g., "about N minutes"):
auto latency = 89'500ms;
auto minutes_ = round<minutes>(latency); // 2min (1.5 rounds to even = 2)When feeding a duration into calendar arithmetic (e.g., adding to a sys_days), always prefer floor over duration_cast to avoid counterintuitive results on timestamps that precede the Unix epoch. For display or user-facing output, round is usually the most natural choice, while ceil suits scheduling and timeout calculations where underestimating a deadline is incorrect.
Monotonic vs Wall Clocks — Portability Caveats
steady_clock is the only standard clock guaranteed to never go backward, making it the correct choice for measuring elapsed time. system_clock represents UTC wall time and is subject to NTP adjustments, leap-second smearing, and manual corrections — any of which can cause it to jump backward or forward mid-measurement.
// C++20
#include <chrono>
using namespace std::chrono;
// ── Elapsed-time measurement — always use steady_clock ────────────────────
{
auto t0 = steady_clock::now();
do_work();
auto dt = steady_clock::now() - t0; // guaranteed non-negative
// duration_cast<milliseconds>(dt).count() is the wall-ms elapsed
}
// ── Wall-clock timestamps — use system_clock ──────────────────────────────
{
// system_clock::now() returns sys_time<nanoseconds> in C++20
// epoch is 1970-01-01T00:00:00 UTC (normative since C++20)
auto now_utc = system_clock::now();
auto secs = floor<seconds>(now_utc);
year_month_day today{floor<days>(secs)}; // today's UTC date
}
// ── high_resolution_clock — avoid in new code ─────────────────────────────
// The standard permits high_resolution_clock to alias system_clock or
// steady_clock. On some MSVC versions it aliased system_clock (non-monotonic).
// Its epoch and resolution are implementation-defined. Use steady_clock for
// benchmarks; use system_clock for timestamps.
//
// Rule of thumb: steady_clock for Δt, system_clock for "what time is it?"
// ── Correlating steady_clock and system_clock ─────────────────────────────
// There is no standard way to get both readings atomically. The best portable
// approximation takes the wall reading before and after the monotonic reading
// and averages, but even that has a race window. C++20 does not provide a
// clock_cast path between steady_clock and system_clock.
auto wall_before = system_clock::now();
auto mono = steady_clock::now();
auto wall_after = system_clock::now();
// Midpoint estimate of the system_clock equivalent of `mono`:
auto half_skew = (wall_after - wall_before) / 2;
auto approx_wall = wall_before + half_skew; // best-effort
// ── Leap seconds: system_clock vs utc_clock ───────────────────────────────
// system_clock uses "Unix time" (leap-second smeared) so it agrees with
// POSIX time(). utc_clock counts every SI second including inserted leap
// seconds and is ahead of system_clock by the current accumulated count.
// clock_cast<utc_clock>(system_clock::now()) bridges this correctly.
auto as_utc = clock_cast<utc_clock>(system_clock::now()); // C++20On Linux, steady_clock is backed by CLOCK_MONOTONIC and system_clock by CLOCK_REALTIME. On macOS they map to mach_absolute_time and gettimeofday respectively. On Windows, steady_clock uses QueryPerformanceCounter and is genuinely monotonic. The practical takeaway is to never subtract two system_clock readings and assume the result is a reliable elapsed duration — use steady_clock for that, and reserve system_clock for producing human-readable timestamps.
See Also
std::chronobasics — C++11 durations, clocks, and time pointsstd::format— complete format specifier referencestd::filesystem::file_time_type— usesfile_clock