C++
C++20
Modern C++ answers date and time with the C++20 additions to <chrono>, a type-safe library in the spirit this guide recommends (it grew out of Howard Hinnant’s date library). Instead of one overloaded type it draws sharp lines: sys_time is an absolute instant on the UTC timeline (system_clock::now() returns one); local_time is the civil, zone-less reading; zoned_time pairs an instant with a named IANA zone so calendar work can respect DST; and the calendar types year_month_day, year_month, weekday model dates directly. Spans are durations, and std::format renders everything.
The central distinction is sys_time versus local_time. A sys_time names a unique moment; a local_time such as 2026-03-29 01:30 carries no zone and so does not identify a moment. You cross between them through a zoned_time constructed with an explicit zone — the compiler will not let you conflate the two. See Instant vs civil time and Time zones vs offsets.
year_month_day names a period — a whole calendar day — while a sys_time names a point. It is also a field type: it can hold 2026/February/31, and .ok() tells you whether the fields form a real date. That honesty is the point — see Points vs periods and the durations pitfall below.
The safe default for “now” is system_clock::now(); store it as a sys_time in RFC 3339 UTC. Reach for zoned_time when a reader or calendar arithmetic needs a zone.
Civil values are not moments — and the library makes you say so
Because local_time and sys_time are different types, the naive/aware confusion is a compile error rather than a silent offset bug. What remains is choosing the right zone, and — unusually among the languages here — deciding what to do at DST gaps and folds, which <chrono> surfaces as exceptions.
Pitfall: A local_time (or a year_month_day) does not identify a moment. Comparing two local_times meant for different zones, or treating one as UTC, gives results off by an offset that shifts across DST transitions. Construct a zoned_time with an explicit zone before comparing or subtracting.
Pitfall: Constructing a zoned_time from a civil time that falls in a DST gap or fold throws nonexistent_local_time / ambiguous_local_time unless you pass a choose::earliest / choose::latest policy. This is safer than silently guessing, but an unhandled throw crashes the program — decide the policy explicitly for civil input. (Separately, calendar arithmetic like year_month_day + months{1} can yield an invalid date, e.g. 2026-02-31; check .ok() and resolve it, for instance with .../last to clamp to the month’s real final day.)
See also: C for the <time.h> layer C++ inherits, and Parsing user input.
Go deeper: conversions, exact vs calendar arithmetic, and the pre-C++20 world
The core types and conversions:
using namespace std::chrono;
auto now = system_clock::now(); // a sys_time (absolute instant)
zoned_time paris{"Europe/Paris", now}; // instant + zone, for reading
local_time civil = paris.get_local_time(); // the wall-clock reading
sys_time back = zoned_time{"Europe/Paris", civil}.get_sys_time(); // civil -> instant
year_month_day today{floor<days>(paris.get_local_time())}; // a calendar day
Exact vs calendar arithmetic — the distinction behind pitfall #2:
using namespace std::chrono;
// The spring-forward night of 2026-03-29 (London) is only 23 hours long:
auto start = zoned_time{"Europe/London", local_days{March/28/2026} + 12h};
auto exact = start.get_sys_time() + 24h; // exactly 24h later
auto sameWall = zoned_time{"Europe/London", local_days{March/29/2026} + 12h}; // "noon tomorrow"
// exact lands at 13:00 wall on the 29th; sameWall is 12:00 — an hour apart,
// because the intervening night lost an hour. (A civil time inside the
// 01:00-02:00 gap would instead throw, per the pitfall above.)
Formatting and parsing. std::format("{:%Y-%m-%dT%H:%M:%S%z}", zt) renders with chrono format specifiers; std::chrono::parse(fmt, value) is a stream manipulator (is >> std::chrono::parse(...)) that parses text back into the matching type — type-directed, so you get the type you asked for. See Formatting for display.
The pre-C++20 world, in the wild. Older code has three shapes. First, C’s <time.h> used directly — time_t, struct tm, strftime — with all the caveats on the C page. Second, the pre-C++20 half of <chrono> (C++11): system_clock/steady_clock, time_point, and duration, but no calendar or zone types — those are exactly what C++20 added. Third, Howard Hinnant’s header-only date library and its tz extension (the direct ancestor of std::chrono’s calendar/zone support) and Boost.DateTime. If you are on C++20 or later, prefer std::chrono; the older libraries draw the same instant/civil lines with different spellings.