13

I'm converting some code over from boost::filesystem to std::filesystem. Previous code used boost::filesystem::last_write_time() which returns a time_t so direct comparison to a time_t object I already held was trivial. By the way, this time_t I hold is read from file contents persisted long ago, so I'm stuck with using this "time since unix epoch" type.

std::filesystem::last_write_time returns a std::filesystem::file_time_type. Is there a portable way to convert a file_time_type to a time_t, or otherwise portably compare the two objects?

#include <ctime>
#include <filesystem>

std::time_t GetATimeInSecondsSince1970Epoch()
{
    return 1207609200;  // Some time in April 2008 (just an example!)
}

int main()
{
    const std::time_t time = GetATimeInSecondsSince1970Epoch();
    const auto lastWriteTime = std::filesystem::last_write_time("c:\\file.txt");

    // How to portably compare time and lastWriteTime?
}

EDIT: Please note that the sample code at cppreference.com for last_write_time states that it's assuming the clock is a std::chrono::system_clock which implements the to_time_t function. This assumption is not always going to be true and isn't on my platform (VS2017).

Galik
  • 47,303
  • 4
  • 80
  • 117
PeteUK
  • 1,062
  • 12
  • 26
  • [The documentation](https://en.cppreference.com/w/cpp/filesystem/file_time_type) makes referece to `to_time_t`, which might do what you want. – tadman Jul 10 '18 at 20:10
  • The C++ library does not specify the means of converting time points on one clock to equivalent time points on a different clock, and in many cases a conversion simply isn't possible. The only option I see is to convert all code that looks at file timestamps to use last_write_time's clock. – Sam Varshavchik Jul 10 '18 at 20:22
  • @tadman Edited question to clarify why I can't access `to_time_t()` – PeteUK Jul 10 '18 at 20:22
  • 1
    @SamVarshavchik Where possible I'm getting time points from filesystem's clock e.g. `std::filesystem::file_time_type::clock::now()`, but I have these pesky Unix Epoch timestamps persisted long ago. I now see that was a poor decision. Guessing I will need to use `stat()`? – PeteUK Jul 10 '18 at 20:26
  • @PeteUK since you are not on Unix filesystem, using Unix timestamp was probably not the best design choice. – SergeyA Jul 10 '18 at 20:33
  • @SergeyA That's probably true. Even if I had stored a the timestamp in a different manner, I think I'd still be incapable of comparing that with what `std::filesystem::last_write_time` currently returns. – PeteUK Jul 10 '18 at 20:44

4 Answers4

12

Fwiw, when C++20 gets here, the portable solution will be:

clock_cast<file_clock>(system_clock::from_time_t(time)) < lastWriteTime

This converts the time_t into file_time as opposed to vice-versa. The advantage of this approach is that file_time typically has a higher precision than time_t. Converting file_time to time_t will loose that precision during the conversion, and thus risk making the comparison inaccurate.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • thanks for pointing out, but isn't that preknowledge something we want to avoid in a standard ? – Jesko Jan 12 '23 at 19:21
  • The chrono additions to C++20 are designed such that you will never need to use the C timing API again (aka `time_t`) except to interface with code that is not using C++ chrono. The analogous C++ 20 comparison would be comparing a `file_time` to a `sys_time` in which case it is the same except you just skip the `from_time_t` step. – Howard Hinnant Jan 12 '23 at 20:07
  • Migration away from the C timing API is strongly encouraged as it is error-prone, hard to use correctly, poorly specified, type-unsafe, and contains a subset of the C++20 chrono functionality. – Howard Hinnant Jan 12 '23 at 20:12
7

I faced the same problem and I solved it using a dedicated code for Visual Studio.

In case of VS, I use the _wstati64 function (w for wide char, because Windows encodes Unicode paths in Utf16) and the wstring convertion of the path class.

The whole thing is gathered in this function:

#if defined ( _WIN32 )
#include <sys/stat.h>
#endif

std::time_t GetFileWriteTime ( const std::filesystem::path& filename )
{
    #if defined ( _WIN32 )
    {
        struct _stat64 fileInfo;
        if ( _wstati64 ( filename.wstring ().c_str (), &fileInfo ) != 0 )
        {
            throw std::runtime_error ( "Failed to get last write time." );
        }
        return fileInfo.st_mtime;
    }
    #else
    {
        auto fsTime = std::filesystem::last_write_time ( filename );
        return decltype ( fsTime )::clock::to_time_t ( fsTime );
    }
    #endif
}
S.Clem
  • 491
  • 5
  • 10
6

The very article you linked shows how to do this: through to_time_t member of the corresponding clock of the file_time_type.

Copy-paste from your own link:

auto ftime = fs::last_write_time(p);
std::time_t cftime = decltype(ftime)::clock::to_time_t(ftime); 

If your platform doesn't give you system_clock as a clock for file_time_type, than there would be no portable solution (until, at least, C++20 when file_time_type clock is standardized). Until than, you'd have to figure out what clock it actually is and than cast time appropriately through duration_cast and friends.

SergeyA
  • 61,605
  • 5
  • 78
  • 137
  • 3
    The article linked just shows an example assuming the clock type is a system_clock. That assumption cannot be made, and on VS2017 it isn't a system_clock, so to_time_t isn't available. I'll edit my question to mention this. – PeteUK Jul 10 '18 at 20:16
  • 2
    @PeteUK I added some wording (explaining that in that case there is no portable solution). – SergeyA Jul 10 '18 at 20:25
  • 1
    Appreciate it. Thanks for considering my issue. – PeteUK Jul 10 '18 at 20:29
0

This is my polyfill which works on Windows, MacOS and Linux. Unfortunately neither Clang nor GCC have file_clock support yet and support on Windows is also bound to recent Windows 10+ versions. See: https://github.com/microsoft/STL/issues/1911

// Copied from Boost: filesystem/src/operations.cpp
// these constants come from inspecting some Microsoft sample code
#ifdef _WIN32
inline std::time_t to_time_t(FILETIME const& ft) BOOST_NOEXCEPT {
    uint64_t t = (static_cast<uint64_t>(ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
    t -= 116444736000000000ull;
    t /= 10000000u;
    return static_cast<std::time_t>(t);
}
#else
// Copied from Stackoverflow
template<typename TP>
std::time_t to_time_t(TP tp) {
    using namespace std::chrono;
    auto sctp = time_point_cast<system_clock::duration>(tp - TP::clock::now() + system_clock::now());
    return system_clock::to_time_t(sctp);
}
#endif

// Use this polyfill until everyone supports C++20
// This code is highly depended on the implementation
time_t PortableLastWriteTime(const std::string& File) {
#ifdef _WIN32
    // We cannot use the C++20 and filesystem due to this incompatibility/error: https://github.com/microsoft/STL/issues/1911
    // This problem makes this request to fail on Windows older than Windows 10 (1903) and somehow Server 2019 completely
    HANDLE handle = CreateFile(File.c_str(), GENERIC_READ, FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, NULL);
    FILETIME lwt;
    if (!::GetFileTime(handle, NULL, NULL, &lwt)) {
        CloseHandle(handle);
        return 0;
    }
    CloseHandle(handle);
    return to_time_t(lwt);
#elif __GLIBCXX__
    auto ftime = std::filesystem::last_write_time(File);
    return to_time_t(ftime);
#elif _LIBCPP_VERSION
    auto ftime = std::filesystem::last_write_time(File);
    return decltype(ftime)::clock::to_time_t(ftime);
#elif __CORRECT_CPP20_VERSION
    // This is the correct C++20 usage when compilers have full compatibility
    const auto fileTime = std::filesystem::last_write_time(File);
    const auto systemTime = std::chrono::clock_cast<std::chrono::system_clock>(fileTime);
    const auto time = std::chrono::system_clock::to_time_t(systemTime);
    return time;
#else
    Unsupported compiler !
#endif
}