The Portable Way to Extract Date and Time from a File's mtime in C++ | Sandor Dargo's Blog<br>Sandor Dargo's Blog<br>On C++, software development and books
HOME TAGS ARCHIVES BOOKS SPEAKING DAILY C++ WORKSHOPS HI... SUBSCRIBE
Blog 2026 07 22 The Portable Way to Extract Date and Time from a File's mtime in C++ Post<br>Cancel
The Portable Way to Extract Date and Time from a File's mtime in C++<br>Sandor Dargo Jul 22 2026-07-22T00:00:00+02:00<br>8 min
After I finished my Time in C++ series, a reader asked a seemingly simple question:<br>“What is the portable way to extract the year, month, day, hour, minute, and second from a file’s modification time? I tried, and I couldn’t get it to work reliably on macOS and Windows.”
Fair question. You call std::filesystem::last_write_time(), you get back a time point, and you want the calendar components. How hard can it be?<br>Harder than it should be. The answer touches three layers of portability problems — different epochs, different conversion APIs, and incomplete standard library implementations. Let’s dig in.<br>The starting point<br>std::filesystem::last_write_time() returns a std::filesystem::file_time_type, which is a time_point based on file_clock — a clock introduced in C++20 specifically for filesystem timestamps. To extract year/month/day and hour/minute/second, you need to convert this to system_clock — a clock whose epoch is known (Unix epoch, January 1, 1970) — and then decompose it into calendar components.<br>Sounds like two steps. In practice, both steps have portability traps.<br>Layer 1: The epoch is implementation-defined<br>The C++ standard says the epoch of file_clock is unspecified. Not even “implementation-defined” — the standard does not require implementations to document it. Each major implementation chose differently:<br>ImplementationEpochRepresentationPeriodlibc++ (Clang)1970-01-01 (Unix)__int128_tnanosecondsMSVC1601-01-01 (Windows FILETIME)long long100 nanosecondslibstdc++ (GCC)2174-01-01 long longnanoseconds<br>Yes, GCC’s epoch is in the future. A signed 64-bit integer with nanosecond resolution covers roughly ±292 years. By centering that range at 2174 instead of 1970, GCC covers the 1882–2466 range — which happens to match what ext4 filesystems need (GCC patch). Current dates simply have negative time_since_epoch() values.<br>The practical consequence: you cannot take time_since_epoch().count() and do manual arithmetic. You’d be off by 204 years on GCC and 369 years on MSVC.<br>Layer 2: The conversion API split<br>C++20 introduced file_clock and said it must provide conversion functions — but gave implementations a choice. file_clock must provide either to_sys()/from_sys() or to_utc()/from_utc(). Not both. Not a common interface.<br>The implementations made opposite choices:<br>GCC/libstdc++Clang/libc++MSVCfile_clock::to_sys()yesyesno file_clock::to_utc()no no yes<br>Code calling file_clock::to_sys() compiles on GCC and Clang but fails on MSVC. Code calling file_clock::to_utc() compiles on MSVC but fails everywhere else. Microsoft’s own documentation explicitly warns about this.<br>Layer 3: clock_cast was supposed to fix this<br>std::chrono::clock_cast is the standard’s portable wrapper. It finds a conversion path through whichever intermediate clock is available — exactly what you’d want here.<br>For a long time, this was the missing piece. P0355R7 — the paper that added calendars, time zones, and clock conversions to C++20 — was only partially implemented in libc++. While calendar types like year_month_day were available and clocks like utc_clock and tai_clock landed in recent versions, clock_cast was simply not there. That meant no single API worked across all three implementations:<br>to_sys() worked on GCC + Clang/libc++, but not MSVCclock_cast worked on GCC + MSVC, but not Clang/libc++The good news: all three major implementations now ship clock_cast, so a single line works everywhere:<br>auto sys_time = std::chrono::clock_caststd::chrono::system_clock>(ftime);
That said, the layers above still matter — they explain why this was hard and why older code and blog posts are full of #ifdef branches and manual conversions. If you’re on a recent toolchain, you can skip straight to the clean solution below.<br>But std::format just works?<br>The C++20 standard actually allows file_time_type to be formatted directly:<br>10<br>11<br>12<br>13<br>14<br>15<br>16<br>17<br>18<br>19<br>20<br>21<br>22<br>23<br>24<br>25<br>26<br>27<br>28<br>29<br>30<br>31<br>32<br>33<br>// https://godbolt.org/z/bneE4xv5s
#include<br>#include<br>#include<br>#include<br>#include<br>#include
bool create_file(const std::string& filename) {<br>std::ofstream file{filename};<br>return file.good();
int main() {<br>std::string file_name{"some_file.txt"};
if (!create_file(file_name)) {<br>std::cerr "Failed to create " file_name "\n";<br>return 1;
std::error_code ec;<br>auto ftime = std::filesystem::last_write_time(file_name, ec);<br>if (ec) {<br>std::cerr "Failed to get mtime: " ec.message() "\n";<br>return 1;
std::cout std::format("{:%Y-%m-%d %H:%M:%S}", ftime) "\n";
return 0;
If all you need is a display string, this is elegant — one line, no manual...