C++ / STANDARD CONTAINERS
Dates and durations with chrono
Model time with chrono: build and validate calendar dates, do day and month arithmetic through sys_days, and convert durations without silent truncation.
What you will learn
- Write durations as typed values like 1500ms instead of bare ints with a unit in a comment
- Use sys_days for day arithmetic and date differences, convert back with year_month_day
- Check ok() after adding months or years, since 2025-01-31 plus one month is 2025-02-31
- Split timestamps with floor<days>, not duration_cast, so pre-1970 values stay correct
Understanding Dates and durations with chrono
A chrono duration is an arithmetic count paired with a compile-time ratio that says what one tick means; milliseconds is duration<long long, ratio<1,1000>>. Because the unit lives in the type, conversions happen for you when nothing can be lost, as when seconds becomes milliseconds by multiplying by 1000, while a lossy direction like milliseconds to seconds is only reachable through duration_cast, floor, ceil or round. That is why 1500ms + 2s compiles and yields 3500ms: both operands convert to the finer common unit, and that conversion is exact.
A time_point is a duration measured from a particular clock's epoch, so it is the same kind of value with a starting line attached. system_clock is Unix time, with an epoch of 1970-01-01 UTC, no leap seconds, and no promise that it moves forward, since the operating system can correct it; steady_clock promises only that it never goes backwards, which is what elapsed-time measurement needs. sys_days is nothing more than a system_clock time_point whose precision is one day, which makes it the serial-number form of a date, so subtracting two of them yields days.
The calendar types are the other half of the picture, and they are field types rather than counts: year, month and day hold their numbers separately and compose through the overloaded slash, as in 2024y/February/28. Nothing validates the fields on construction, so ok() is your check, and month or year arithmetic operates on the fields and can hand you 2025-02-31. Converting to sys_days switches from fields to a day count and constructing a year_month_day from sys_days switches back; a day count cannot represent a date that does not exist, which is exactly why day arithmetic has to make that round trip.
<chrono>
<iostream>
int main()
{
using namespace std::chrono;
// A duration is a count plus a unit fixed at compile time.
milliseconds timeout = 1500ms;
// seconds whole = timeout; // error: that conversion would truncate
std::cout << timeout.count() << " ms\n";
std::cout << duration_cast<seconds>(timeout).count() << " s (truncated)\n";
std::cout << duration<double>(timeout).count() << " s (exact)\n";
// Mixing units is fine when the common unit loses nothing.
auto total = timeout + 2s; // common type is milliseconds
std::cout << total.count() << " ms total\n";
// A calendar date is fields, not a count.
year_month_day due = 2024y/February/28;
std::cout << due << " is a " << weekday{sys_days{due}} << '\n';
// Day arithmetic goes through the serial form, sys_days.
year_month_day later = sys_days{due} + days{2};
std::cout << "two days later: " << later << '\n';
days gap = sys_days{2024y/March/1} - sys_days{due};
std::cout << "gap: " << gap.count() << " days\n";
}In chrono the unit and the meaning are part of the type: durations count a compile-time unit, time_points count from an epoch, and calendar dates are field types you convert to sys_days whenever you need arithmetic.
Worked examples
Splitting a timestamp into date and time of day
Shows why floor<days> and not duration_cast is the correct way to cut a time_point at a day boundary.
<chrono>
<iostream>
int main()
{
using namespace std::chrono;
// A time_point is a duration counted from the clock's epoch.
sys_seconds t = sys_days{1969y/July/16} + 13h + 32min;
std::cout << "epoch offset: " << t.time_since_epoch().count() << " s\n";
sys_days floored = floor<days>(t); // rounds toward the past
sys_days truncated = time_point_cast<days>(t); // rounds toward the epoch
std::cout << "floor: " << year_month_day{floored}
<< ' ' << hh_mm_ss{t - floored} << '\n';
std::cout << "cast: " << year_month_day{truncated}
<< " remainder " << (t - truncated).count() << " s\n";
}Example explained
Line 1sys_days{1969y/July/16} holds a negative count because the date is 169 days before the 1970 epoch.
Line 2floor<days> always moves to the earlier day boundary, so t - floored stays inside [0h, 24h).
Line 3time_point_cast truncates toward zero like duration_cast, which for a negative count lands on the next day and leaves a negative remainder.
Line 4hh_mm_ss takes the sub-day duration and prints it as hours, minutes and seconds.
Adding a month can produce a date that does not exist
Demonstrates that months arithmetic works on the day field and needs an ok() check plus clamping.
<chrono>
<iostream>
int main()
{
using namespace std::chrono;
std::cout << std::boolalpha;
year_month_day jan31 = 2025y/January/31;
year_month_day next = jan31 + months{1}; // changes the month, keeps the day
std::cout << next << " (ok: " << next.ok() << ")\n";
year_month_day clamped = next.ok() ? next
: year_month_day{next.year()/next.month()/last};
std::cout << "clamped: " << clamped << '\n';
year_month_day rolled = sys_days{jan31} + days{31};
std::cout << "31 days later: " << rolled << " (ok: " << rolled.ok() << ")\n";
}Example explained
Line 1jan31 + months{1} is field arithmetic: February replaces January and the day stays 31, giving a date nobody can have.
Line 2The stream inserter itself tells you the date is invalid, but only ok() lets your code react to it.
Line 3next.year()/next.month()/last names the last day of that month, which is the usual way to clamp a monthly schedule to 2025-02-28.
Line 4Adding days requires the sys_days round trip and can never produce an invalid date, but it counts real days rather than months.
Integer durations divide like integers
Shows how the representation type, not the unit alone, decides whether a division keeps its fraction.
<chrono>
<iostream>
<ratio>
int main()
{
using namespace std::chrono;
std::cout << (1s / 60).count() << " s per frame\n";
std::cout << (1000ms / 60).count() << " ms per frame\n";
std::cout << duration<double, std::milli>(1s / 60.0).count() << " ms per frame\n";
std::cout << round<milliseconds>(1s / 60.0).count() << " ms per frame (rounded)\n";
}Example explained
Line 11s / 60 divides a long long count of whole seconds, so the frame time collapses to zero.
Line 21000ms / 60 keeps the millisecond unit and gives 16, silently dropping two thirds of a millisecond per frame.
Line 3Dividing by 60.0 promotes the representation to double, and duration<double, std::milli> can then hold 16.6667.
Line 4round<milliseconds> picks the nearest whole millisecond rather than truncating, which is why it reports 17.
Important notes
system_clock models Unix time, so it skips leap seconds and can be adjusted in either direction; use steady_clock for intervals and keep sys_days and year_month_day for civil dates.
The calendar types, the y and d literals, and these stream inserters are C++20, so compile with -std=c++20 and a recent libstdc++ or libc++ or 2024y/February/28 will not resolve.
Common mistakes
Printing (t1 - t0).count() and labelling the number milliseconds: steady_clock's period is implementation-defined and is nanoseconds on most builds, so the reported figure is off by a factor of a million until you duration_cast<milliseconds> it.
Building a monthly schedule with year_month_day + months{1} and never calling ok(): 2025-01-31 becomes 2025-02-31, and converting an invalid date to sys_days gives an unspecified day, so the schedule quietly walks into March.
Measuring elapsed time with system_clock::now(): a clock correction during the measurement can shorten the interval or make it negative, and no amount of casting will recover the real duration.
Try it yourself
Change, predict, then run
In a browser editor, print how many days separate 2000-01-01 from 2026-09-03 and which weekday the later date falls on, using sys_days for both. Then add months{6} to 2026-08-31, print the result and its ok() flag, and clamp it with year/month/last when it is invalid.
Open the C++ workspaceCheck your understanding
Why does seconds s = milliseconds{1500}; fail to compile while auto total = milliseconds{1500} + seconds{2}; compiles and gives 3500?
- Implicit duration conversion is allowed only when the target unit cannot lose a value; milliseconds to seconds can lose one, seconds to milliseconds cannot, and the sum uses milliseconds as the common unit.
- seconds and milliseconds are unrelated types, and only operator+ is overloaded to accept mixed units.
- operator+ quietly applies duration_cast to the milliseconds operand, while assignment refuses to do so.
- 1500 milliseconds does not fit in the representation of seconds, whereas the sum's 3500 does.
Show answer
Conversion between durations is implicit exactly when the target period can represent every value of the source without division; milliseconds to seconds divides by 1000 and would drop 500ms, so it needs duration_cast. The sum works because common_type picks milliseconds and converts the seconds operand upward by multiplying by 1000, which is exact, so nothing truncating is inserted for you; that is why the option claiming operator+ applies a duration_cast is wrong.