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.
See Also
std::chronobasics — C++11 durations, clocks, and time pointsstd::format— complete format specifier referencestd::filesystem::file_time_type— usesfile_clock