C++20/23 Chrono: Time Zones, Calendars, and Duration Mastery
Master C++20/23 chrono library: time zones, calendars, durations.
20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.
- ✓Basic knowledge of C++ templates and type traits.
- ✓Familiarity with C++11/14 chrono basics (duration, time_point).
- ✓Understanding of time zones and UTC.
- C++20 introduces
std::chrono::time_zone,std::chrono::zoned_time, and calendar types likeyear_month_day. - Use
std::chrono::current_zone()to get the local time zone andstd::chrono::locate_zone()for named zones. - Calendar arithmetic is done with
year_month_dayandyear_month_day_lastfor end-of-month operations. - Duration conversions use
std::chrono::duration_castorstd::chrono::floorfor rounding. - C++23 adds
std::chrono::parsefor string-to-time_point parsing.
Imagine you have a universal clock that ticks everywhere at the same rate. But different cities have their own local times. The C++20 chrono library is like a smart travel assistant that knows all time zones, can convert between them, and even handles tricky things like leap years and daylight saving. It also gives you a calendar that can tell you the date of the third Tuesday of a month, just like a pocket planner.
Time is a fundamental dimension in software, yet handling it correctly remains one of the most error-prone tasks. From scheduling tasks in distributed systems to logging events across time zones, developers constantly grapple with time zones, daylight saving transitions, and calendar arithmetic. Before C++20, the standard library offered only basic duration and time_point facilities, leaving time zone and calendar support to third-party libraries like Howard Hinnant's date library. C++20 integrated these features directly into std::chrono, providing a type-safe, high-performance, and portable solution. C++23 further enhances the library with parsing capabilities. This tutorial dives deep into the chrono extensions, covering time zones, calendars, durations, and their interplay. You'll learn how to construct zoned_time objects, perform calendar arithmetic, handle leap years, and avoid common pitfalls like double-counting daylight saving adjustments. We'll also explore production debugging techniques and a real-world incident where a time zone bug caused a global outage. By the end, you'll be equipped to write robust, time-aware C++ applications.
1. Time Zones: The Foundation
C++20 introduces the <chrono> header with time zone support via the IANA time zone database. The key types are std::chrono::time_zone, std::chrono::zoned_time, and std::chrono::local_time. A time_zone represents a named time zone (e.g., "America/New_York"). You obtain it via std::chrono::locate_zone("zone_name") or std::chrono::current_zone(). A zoned_time pairs a time zone with a time_point (typically sys_time which is UTC). It provides methods to get the local time, the sys time, and the time zone abbreviation. Importantly, zoned_time handles daylight saving time automatically: when you construct it with a sys_time, it converts to local time; when you construct it with a local_time, it requires an explicit choose parameter (earliest or latest) to resolve ambiguity during fall-back transitions. Example:
```cpp #include <chrono> #include <iostream>
int main() { using namespace std::chrono; auto zt = zoned_time{current_zone(), system_clock::now()}; std::cout << "Local time: " << zt.get_local_time() << ' '; std::cout << "UTC time: " << zt.get_sys_time() << ' '; std::cout << "Time zone abbreviation: " << zt.get_info().abbrev << ' '; } ```
Output: `` Local time: 2025-03-15 14:30:00.123456 UTC time: 2025-03-15 18:30:00.123456 Time zone abbreviation: EDT ``
When converting from a local time to sys time, you must handle ambiguity. Use choose::earliest or choose::latest to specify which occurrence to pick during fall-back. Example:
``cpp auto local = local_days{2021y/November/7} + 1h + 30min; // 1:30 AM local auto zt = zoned_time{"America/New_York", local, choose::earliest}; std::cout << ``zt.get_sys_time() << ' '; // 5:30 UTC (first occurrence)
Always prefer zoned_time over manual offset calculations to avoid DST bugs.
zoned_time to safely handle time zone conversions and DST transitions.2. Calendars: year_month_day and Friends
C++20 chrono provides calendar types: year, month, day, year_month, month_day, year_month_day, and year_month_day_last. These are compile-time constants and support arithmetic. The year_month_day type represents a specific date. You can construct it using the y/m/d literal syntax: 2021y/January/1. The year_month_day_last type represents the last day of a month (e.g., 2021y/February/last). This is crucial for handling month ends correctly. You can also get the last day of a month via year_month_day_last::day(). Example:
```cpp #include <chrono> #include <iostream>
int main() { using namespace std::chrono; auto date = 2025y/March/15; std::cout << date << ' '; // 2025-03-15 auto last_day = 2025y/February/last; std::cout << last_day << ' '; // 2025-02-28 (not leap year) // Add months: use year_month auto ym = 2025y/March; auto next_month = ym + months{1}; // 2025/April std::cout << year_month_day{next_month/15} << ' '; // 2025-04-15 // Difference in days auto d1 = 2025y/March/15; auto d2 = 2025y/March/20; auto diff = sys_days{d2} - sys_days{d1}; std::cout << diff.count() << " days "; // 5 } ```
Output: `` 2025-03-15 2025-02-28 2025-04-15 5 days ``
Calendar arithmetic respects month lengths and leap years. Adding months to a date like January 31 yields February 28 (or 29 in leap year) because the day is clamped. Use year_month_day_last if you need the last day of the month.
year_month_day arithmetic to avoid off-by-one errors on month boundaries.3. Duration: Precision and Conversions
Duration is the foundation of chrono. It represents a time span with a tick period. Common types: std::chrono::nanoseconds, microseconds, milliseconds, seconds, minutes, hours, days, weeks, months, years. Conversions between durations are explicit via duration_cast or implicit if lossless. Use std::chrono::floor, ceil, round for rounding to a specific duration. Example:
```cpp #include <chrono> #include <iostream>
int main() { using namespace std::chrono; auto ms = 1234ms; auto sec = duration_cast<seconds>(ms); std::cout << ms << " = " << sec << ' '; // 1234ms = 1s // Floor to seconds auto floored = floor<seconds>(ms); std::cout << "Floor: " << floored << ' '; // 1s // Round to nearest second auto rounded = round<seconds>(ms); std::cout << "Round: " << rounded << ' '; // 1s (since 1234ms < 1500ms) // Using days auto d = days{5}; auto hours_in_days = duration_cast<hours>(d); std::cout << d << " = " << hours_in_days << ' '; // 5d = 120h } ```
Output: `` 1234ms = 1s Floor: 1s Round: 1s 5d = 120h ``
When working with time_points, you can add durations directly. For calendar durations like months and years, addition to year_month_day works as expected, but adding months to a sys_days is not allowed directly; you must convert to a calendar type first.
steady_clock to avoid issues with system clock adjustments. Convert to milliseconds for API calls expecting integer milliseconds.floor, ceil, round for controlled rounding.4. Time Points and Clocks
C++20 provides three clocks: system_clock (wall clock, UTC), steady_clock (monotonic, for intervals), and high_resolution_clock (usually alias of steady_clock). A time_point is a point in time relative to a clock's epoch. system_clock's epoch is January 1, 1970 00:00:00 UTC (Unix epoch). steady_clock's epoch is typically system boot. You can convert system_clock::time_point to time_t via system_clock::to_time_t, but prefer using zoned_time for display. C++20 introduces sys_days (a time_point with day precision) and local_days for local time. Example:
```cpp #include <chrono> #include <iostream>
int main() { using namespace std::chrono; auto now = system_clock::now(); auto now_days = floor<days>(now); sys_days sd = now_days; std::cout << "Today (UTC): " << sd << ' '; // Convert to local time auto zt = zoned_time{current_zone(), now}; auto local_dp = floor<days>(zt.get_local_time()); std::cout << "Today (local): " << local_dp << ' '; // Steady clock for timing auto t1 = steady_clock::now(); // ... work auto t2 = steady_clock::now(); auto diff = t2 - t1; std::cout << "Elapsed: " << diff << ' '; } ```
Output: `` Today (UTC): 2025-03-15 Today (local): 2025-03-15 Elapsed: 12345ns ``
When you need to represent a date without time, use sys_days or local_days. They are time_points with day precision and can be converted to year_month_day.
sys_days or system_clock::time_point in UTC. Convert to local time only when displaying to users.sys_days for date-only UTC times and local_days for local dates. Always prefer system_clock for absolute timestamps.5. Parsing and Formatting with C++23
C++23 introduces std::chrono::parse for parsing strings into time_points and durations. It uses format strings similar to std::format. The parse function is a manipulator for streams. Example:
```cpp #include <chrono> #include <iostream> #include <sstream>
int main() { using namespace std::chrono; std::istringstream iss("2025-03-15 14:30:00"); sys_seconds tp; iss >> parse("%F %T", tp); if (iss) { std::cout << "Parsed: " << tp << ' '; } else { std::cout << "Parse failed "; } // Parse with time zone std::istringstream iss2("2025-03-15 10:30:00 America/New_York"); zoned_time<seconds> zt; iss2 >> parse("%F %T %Z", zt); if (iss2) { std::cout << "Zoned time: " << zt << ' '; } } ```
Output: `` Parsed: 2025-03-15 14:30:00 Zoned time: 2025-03-15 10:30:00 America/New_York ``
Formatting is done via std::format or stream insertion. The std::formatter specialization for chrono types allows custom formats. Example:
```cpp #include <chrono> #include <iostream> #include <format>
int main() { using namespace std::chrono; auto now = system_clock::now(); std::cout << std::format("{:%Y-%m-%d %H:%M:%S}", now) << ' '; auto zt = zoned_time{current_zone(), now}; std::cout << std::format("{:%Y-%m-%d %H:%M:%S %Z}", zt) << ' '; } ```
Output: `` 2025-03-15 18:30:00 2025-03-15 14:30:00 EDT ``
Note: std::format for chrono is available in C++20 but with limited support in some compilers. C++23 standardizes parse.
parse with sys_seconds to ensure unambiguous UTC interpretation.parse simplifies string-to-time conversion; use std::format for flexible output.6. Advanced: Custom Time Zones and Leap Seconds
C++20 chrono supports custom time zones via the std::chrono::time_zone class, but you typically use the built-in IANA database. For leap seconds, the library models them as part of the UTC timeline. The sys_time includes leap seconds, but they are transparent to most users. If you need to handle leap seconds explicitly, you can use std::chrono::leap_second and std::chrono::get_tzdb().leap_seconds. Example:
```cpp #include <chrono> #include <iostream>
int main() { using namespace std::chrono; const auto& tzdb = get_tzdb(); std::cout << "Leap seconds: "; for (const auto& ls : tzdb.leap_seconds) { std::cout << ls.date() << ' '; } // Check if a time_point is a leap second auto tp = sys_days{2016y/December/31} + 23h + 59min + 60s; // 2016-12-31 23:59:60 UTC std::cout << "Is leap second? " << std::boolalpha << (tp.time_since_epoch().count() % 86400 == 0) << ' '; } ```
Output (partial): `` Leap seconds: 1972-06-30 23:59:60 1972-12-31 23:59:60 ... 2016-12-31 23:59:60 Is leap second? true ``
Custom time zones can be created by deriving from std::chrono::time_zone, but it's rarely needed. For most applications, the IANA database suffices.
steady_clock for intervals to avoid jumps.The 2 AM Ghost: A Daylight Saving Time Outage
system_clock::now() returns local time, and that adding 24 hours to a time_point would always yield the next day's same local time.now + 24h in system_clock time, which is UTC. When converting back to local time for display, it produced a non-existent time (2:30 AM EDT). The scheduler then skipped that time because it never occurred in local time.zoned_time with current_zone() and store the next run time as a zoned_time or as a local time point (local_time) to preserve the intended local time. Alternatively, always work in UTC and convert only for display.- Never assume
system_clock::now()returns local time; it returns UTC. - When scheduling recurring events, store the next run time in local time or as a
zoned_timeto handle DST transitions correctly. - Always test your time-related code around DST changeover dates.
- Use
std::chrono::floorwithdaysto truncate to a date boundary safely. - Prefer
zoned_timeover manual time zone offset calculations.
zoned_time to represent local time. Verify with a test case for the DST change date.duration_cast and check for overflow.std::chrono::parse.std::chrono::parse with a std::chrono::sys_time or local_time variable.year_month_day_last for end-of-month operations. Validate dates with .ok() before use.auto zt = zoned_time{current_zone(), system_clock::now()};cout << zt.get_local_time();system_clock::to_time_t with zoned_time for display.| File | Command / Code | Purpose |
|---|---|---|
| timezone_example.cpp | int main() { | 1. Time Zones |
| calendar_example.cpp | int main() { | 2. Calendars |
| duration_example.cpp | int main() { | 3. Duration |
| timepoint_example.cpp | int main() { | 4. Time Points and Clocks |
| parse_format_example.cpp | int main() { | 5. Parsing and Formatting with C++23 |
| advanced_example.cpp | int main() { | 6. Advanced |
Key takeaways
zoned_time for time zone conversions and DST handling.steady_clock for measuring intervals; use system_clock for absolute timestamps.year_month_day provide safe date arithmetic with leap year support.parse simplifies string-to-time conversion; use std::format for flexible output.Common mistakes to avoid
3 patternsUsing `system_clock::now()` for measuring elapsed time.
Adding `months` directly to a `sys_days`.
Assuming `system_clock::to_time_t` returns local time.
Interview Questions on This Topic
Explain the difference between `sys_time` and `local_time` in C++20 chrono.
sys_time is a time_point associated with the system clock (UTC). local_time is a time_point that is not associated with any time zone; it represents a local time without an offset. To convert local_time to sys_time, you need a time zone and must handle DST ambiguity.Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.
That's C++ Advanced. Mark it forged?
5 min read · try the examples if you haven't