Table of Contents

In this blog post, we’ll examine the potentially simple task of getting and displaying file time. Usually, we can depend on some library or OS-specific code to get that file property. Fortunately, since C++20, we have a reliable and simple technique from the std::filesystem and std::chrono components.

Let’s jump in.

Before C++17  

In C++, working with files was usually reserved for OS APIs and third-party libraries. Here are two examples of such an approach:

Linux/POSIX Version  

In Linux or any POSIX-compliant system, you can use the stat function to get file times.

#include <iostream>
#include <fstream>
#include <sys/stat.h>
#include <ctime>

int main() {
    const char* file_path = "example.txt";
    std::ofstream(file_path) << "Hello, World!";

    struct stat file_stat;

    if (stat(file_path, &file_stat) == 0) {    
        std::time_t mod_time = file_stat.st_mtime;
        char* str = std::asctime(std::localtime(&mod_time));
        std::cout << "Last modification time: " << str;
    }
    else
        std::cerr << "Error getting file status\n";
}

Run @Compile Explorer

Windows/MSVC Version  

In Windows, you can use the GetFileTime function to get file times.

#include <iostream>
#include <windows.h>
#include <fstream>
#include <ctime>

void printFileTime(const FILETIME& ft) {
    SYSTEMTIME st;
    FileTimeToSystemTime(&ft, &st);

    std::tm tm = {};
    tm.tm_year = st.wYear - 1900;
    tm.tm_mon = st.wMonth - 1;
    tm.tm_mday = st.wDay;
    tm.tm_hour = st.wHour;
    tm.tm_min = st.wMinute;
    tm.tm_sec = st.wSecond;

    std::time_t time = std::mktime(&tm);
    std::cout << "Last modification time: " << std::asctime(std::localtime(&time));
}

int main() {
    const char* file_path = "example.txt";
    std::ofstream(file_path) << "Hello, World!";

    HANDLE hFile = CreateFile(file_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

    if (hFile != INVALID_HANDLE_VALUE) {
        FILETIME ftCreate, ftAccess, ftWrite;

        if (GetFileTime(hFile, &ftCreate, &ftAccess, &ftWrite))
            printFileTime(ftWrite);
        else
            std::cerr << "Error getting file time\n";

        CloseHandle(hFile);
    }
    else
        std::cerr << "Error opening file\n";
}

Try @Compiler Explorer

C++17 and the std::filesystem  

Finally, in C++17, we got std::filesystem, which is based on the well-tested boost::filesystem. Inside this package, we have the filesystem::last_write_time() functions:

We have one free function and a member function in directory_entry. They both return file_time_type which in C++17 is defined as:

// C++17:
using file_time_type = std::chrono::time_point</*trivial-clock*/>;

However, this is a bit inconvenient:

From the standard, 30.10.25 Header <filesystem> synopsis:

A> trivial-clock is an implementation-defined type that satisfies the TrivialClock requirements and that is capable of representing and measuring file time values. Implementations should ensure that the resolution and range of file_time_type reflect the operating system dependent resolution and range of file time values.

In other words, it’s implementation-dependent.

For example, in the GCC/Clang STL file, time is implemented on top of chrono::system_clock, but in MSVC, it’s a platform-specific clock.

Here are some more details about the implementation decisions in Visual Studio: std::filesystem::file_time_type does not allow easy conversion to time_t

Let’s have a look at some code.

auto filetime = fs::last_write_time(myPath);
const auto toNow = fs::file_time_type::clock::now() - filetime;
const auto elapsedSec = duration_cast<seconds>(toNow).count();
// skipped std::chrono prefix for duration_cast and seconds

The above code gives you a way to compute the number of seconds since the last update. However, this is not as useful as showing a real date.

On POSIX (GCC and Clang Implementation), you can easily convert file time to system_clock and then obtain std::time_t:

auto filetime = fs::last_write_time(myPath);
std::time_t convfiletime = std::chrono::system_clock::to_time_t(filetime);
std::cout << "Updated: " << std::ctime(&convfiletime) << '\n';

In MSVC, the code won’t compile. However, there’s a guarantee that file_time_type is usable with native OS functions that takes FILETIME. So we can write the following code to solve the issue:

auto filetime = fs::last_write_time(myPath);
FILETIME ft;
memcpy(&ft, &filetime, sizeof(FILETIME)); // << !
SYSTEMTIME  stSystemTime;
if (FileTimeToSystemTime(&ft, &stSystemTime)) { 
    // use stSystemTime.wYear, stSystemTime.wMonth, stSystemTime.wDay, ...
    // see printFileTime from the example above...
}

Notice the use of memcpy to map filetime onto the ft object - a form of type erasure.

C++20 - finally portable!  

The situation improved, as in C++20, we got std::chrono::file_clock and conversion routines between clocks. See P0355

// C++17
using file_time_type = std::chrono::time_point</*trivial-clock*/>;

// C++20
using file_time_type = std::chrono::time_point<std::chrono::file_clock>;

In short, as of C++20, file time is now well-defined and not implementation-specific. This allows us to write short and portable code.

Here’s a simple version of our example:

#include <iostream>
#include <fstream>
#include <filesystem>
#include <format>
#include <chrono>

int main() {
    auto temp = std::filesystem::temp_directory_path() / "example.txt";
    std::ofstream(temp.c_str()) << "Hello, World!";

    auto ftime = std::filesystem::last_write_time(temp);
    // format
    std::cout << std::format("File write time is {0:%R} on {0:%F}\n", ftime);
    // or <<
    std::cout << ftime;

    std::filesystem::remove(temp);
}

Run @Compiler Explorer

The example creates a temp directory with a temporary file and then writes some strings into it. We fetch the last write time by using the fs::last_write_time() non-member function, which returns the time_point<file_clock>. This time point object can be printed via std::format or the stream operator.

Homework - sorting files by time  

Using the functionality of file_clock, can you implement a program that lists all files in a current directory sorted by date?

Here’s the starting code:

#include <iostream>
#include <fstream>
#include <filesystem>
#include <format>
#include <map>
#include <algorithm>
#include <chrono>
#include <thread>

namespace fs = std::filesystem;

int main() {
    std::vector<fs::path> toDelete;
    
    // create some files...
    for (int i = 0; i < 5; ++i)
    {
        toDelete.emplace_back(std::format("example{}.txt", i));
        std::ofstream(toDelete.back().c_str()) << "Hello, World!";
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }

    // your code here...

    for (auto& f : toDelete)
        fs::remove(f);
}

Start from this @Compiler Explorer example.

Here’s the expected and possible output:

01:45:59 on 2022-03-25, /app/example.cpp
01:46:05 on 2022-03-25, /app/output.s
01:46:05 on 2022-03-25, /app/compilation-result.json
01:46:05 on 2022-03-25, /app/example0.txt
01:46:06 on 2022-03-25, /app/example1.txt
01:46:07 on 2022-03-25, /app/example2.txt
01:46:08 on 2022-03-25, /app/example3.txt
01:46:09 on 2022-03-25, /app/example4.txt

Summary  

C++20 is a big standard revision that brings us lots of powerful features. Fortunately, it also updates existing parts, like better definitions for file_clock. Thanks to it, we could move from vendor-specific code into cross-platform solutions.

Back to you

  • Do you use std::filesystem?
  • What common operations do you do on files through code?

Share comments below