C++ - 如何以独立于平台、线程安全的方式以用户首选的日期/时间语言环境格式格式化文件的最后修改日期和时间

Posted

技术标签:

【中文标题】C++ - 如何以独立于平台、线程安全的方式以用户首选的日期/时间语言环境格式格式化文件的最后修改日期和时间【英文标题】:C++ - How to format a file's last modified date and time in the user's preferred date/time locale format in a platform-independent, thread-safe manner 【发布时间】:2021-11-07 08:38:33 【问题描述】:

我想检索给定文件的最后修改日期和时间,并将其格式化为字符串。我想以线程和内存安全的方式这样做;跨平台兼容;并且不需要使用自定义库。日期和时间应根据操作系统返回的用户首选区域设置和日期/时间格式进行格式化。

使用 C++11/14 功能很好。使用 Boost 也是如此。执行速度并不特别重要。

这是我目前拥有的:

    std::string text;
    text += filename;
    text  = "Savegame: "+text+lf+"_________________"+lf;
    
        text += "Saved on: ";
        const boost::filesystem::path file_name_pathfilename;
        const boost::filesystem::path save_dir_pathgetSaveDir();
        const boost::filesystem::path& full_file_path = boost::filesystem::absolute(file_name_path, save_dir_path);
        std::time_t last_saved_time;
        std::stringstream last_saved_string_stream;
        last_saved_string_stream.exceptions(std::ios_base::failbit);
        try
        
            std::locale users_preferred_localestd::locale("");
            boost::gregorian::date_facet output_facet;
            last_saved_string_stream.imbue(users_preferred_locale);
            last_saved_string_stream.imbue(std::locale(last_saved_string_stream.getloc(), &output_facet));
            output_facet.format("%x");
            last_saved_time = boost::filesystem::last_write_time(full_file_path);
            auto last_saved_time_pointboost::chrono::system_clock::from_time_t(last_saved_time);
            last_saved_string_stream << last_saved_time_point;
        
        catch (boost::filesystem::filesystem_error& fse)
        
            VS_LOG_AND_FLUSH(fatal, (boost::format("Filesystem Error determining savegame's last saved date/time: %1%") % fse.what()));
        
        catch (std::exception& e)
        
            VS_LOG_AND_FLUSH(fatal, (boost::format("General Error determining savegame's last saved date/time: %1%") % e.what()));
        
        catch (...)
        
            VS_LOG_AND_FLUSH(fatal, "Really bad error determining savegame's last saved date/time! What just happened??");
        
        text += last_saved_string_stream.str() + lf;
    

VS_LOG_AND_FLUSH 是一个宏(我知道,我知道),它使用指定的日志级别和消息调用 BOOST_LOG_TRIVIAL,然后刷新所有 boost 日志接收器,以及 stdout 和 stderr。

getSaveDir() 的实现对于这个问题并不重要。让我们接受它返回一个std::string,其中包含存储该程序的存档游戏的目录。

即使走到这一步也比原本应该的要困难得多。我发现实际使用这些库的人的例子非常少——尤其是一起使用。

此代码可以编译,但在 Windows 上运行时会静默崩溃。同时,在 Linux 上,它会运行,但将文件的时间戳显示为自 1970 年 1 月 1 日以来的纳秒数。这不是正确的格式。例如,格式应该类似于美国的“9/10/2021 5:30 PM”或某些欧洲国家/地区的“10/09/2021 17:30:00”。

我做错了什么?而我应该怎么做?

我在output_facet.format(... 行上尝试了几种不同的格式字符串。我试过"%c""%x",结果都一样。

【问题讨论】:

【参考方案1】:

首先:事实证明,getSaveDir() 毕竟并没有可靠地返回预期值。现在已修复。

第二:我的catch 子句依赖于更多格式化的字符串输出,在某些字符串输出已经失败之后。不一定是个好主意。

第三:我的catch 子句实际上并没有退出程序。 VS_LOG_AND_FLUSH 不包含此功能。

第四:我在这个 .cpp 文件顶部包含的特定头文件集很重要。甚至顺序也可能很重要。这是我发现的工作:

#include <boost/filesystem.hpp>
#include <boost/chrono/time_point.hpp>
#include <boost/chrono/io/time_point_io.hpp>
#include <boost/chrono/chrono.hpp>

#include <iostream>
#include <sstream>
#include <chrono>
#include <locale>

最后:我要找的具体输出函数是boost::chrono::time_fmt

这是我现在拥有的代码:

    string text;
    text += filename;
    text  = "Savegame: "+text+lf+"_________________"+lf;
    try
    
        text += "Saved on: ";
        const boost::filesystem::path file_name_pathfilename;
        const boost::filesystem::path save_dir_pathgetSaveDir();
        const boost::filesystem::path full_file_pathboost::filesystem::absolute(file_name_path, save_dir_path);
        std::time_t last_saved_timeboost::filesystem::last_write_time(full_file_path);
        boost::chrono::system_clock::time_point last_saved_time_pointboost::chrono::system_clock::from_time_t(last_saved_time);
        std::ostringstream last_saved_string_stream;
        last_saved_string_stream << boost::chrono::time_fmt(boost::chrono::timezone::local, "%c")
                                 << last_saved_time_point;
        text += last_saved_string_stream.str() + lf;
    
    catch (boost::filesystem::filesystem_error& fse)
    
        VS_LOG_AND_FLUSH(fatal, "boost::filesystem::filesystem_error encountered");
        VS_LOG_AND_FLUSH(fatal, fse.what());
        VSExit(-6);
    
    catch (std::exception& e)
    
        VS_LOG_AND_FLUSH(fatal, "std::exception encountered");
        VS_LOG_AND_FLUSH(fatal, e.what());
        VSExit(-6);
    
    catch (...)
    
        VS_LOG_AND_FLUSH(fatal, "unknown exception type encountered!");
        VSExit(-6);
    

这似乎在 Windows 和 Linux 上都能可靠地工作。 (代码尚未在 macOS 上测试,但至少可以在 Mac 上编译。)

【讨论】:

是的,您涵盖了我的一些代码审查评论,其中一些我在回答中提到了。不过请务必查看该答案,因为您错过了Undefined Behaviour 的最重要原因【参考方案2】:

你已经接近了。一些注意事项:

facet 由它们的语言环境所有,您不应该在那里获取局部变量的地址:

 boost::gregorian::date_facet output_facet;
 last_saved_string_stream.imbue(std::locale(last_saved_string_stream.getloc(), &output_facet));
 output_facet.format("%x");

应该是

 last_saved_string_stream.imbue(std::locale(last_saved_string_stream.getloc(),
     new boost::gregorian::date_facet("%x")));

事实上,多余的灌输可能会被合并:

 std::stringstream last_saved_string_stream;
 last_saved_string_stream.exceptions(std::ios_base::failbit);

 std::locale users_preferred_localestd::locale("");
 last_saved_string_stream.imbue(users_preferred_locale);

     last_saved_string_stream.imbue(std::locale(last_saved_string_stream.getloc(), new boost::gregorian::date_facet("%x")));

可以变成

 std::stringstream ss;
 ss.exceptions(std::ios_base::failbit);

 ss.imbue(std::locale(std::locale(""), new boost::gregorian::date_facet("%x")));

请注意,您可以减少 stringstream 和 time_t 变量的多余范围

转换为 chrono time_point 没有帮助。这些方面仅适用于 Boost DateTime 库类型,而不适用于 Chrono。而是转换为ptimelocal_date_time

 ss << boost::posix_time::from_time_t(
     fs::last_write_time(full_file_path));

为什么要将字符串连接、std iostreams 和 boost::format 结合在一个函数中?

根据我的测试,你或许可以不用灌输:

Live On Coliru

#include <boost/filesystem.hpp>
#include <boost/format.hpp>
#include <boost/chrono.hpp>
#include <boost/date_time.hpp>
namespace fs = boost::filesystem;
using namespace std::string_literals;

// mocks
enum severity  fatal ;
template <typename Msg>
static void VS_LOG_AND_FLUSH(severity, Msg const& msg)  std::clog << msg << std::endl; 
fs::path getSaveDir()  return fs::current_path(); 
static constexpr char const* lf = "\n";

std::string DescribeSaveGame(std::string filename, bool do_imbue)

    std::stringstream ss;
    ss.exceptions(std::ios_base::failbit);
    if (do_imbue) 
        ss.imbue(std::locale(std::locale(""),
                    new boost::gregorian::date_facet("%x")));
    

    ss << "Savegame: " << filename << lf << "_________________" << lf;
    try 
        ss << "Saved on: ";
        ss << boost::posix_time::from_time_t(
                  fs::last_write_time(fs::absolute(filename, getSaveDir())))
           << lf;

        return ss.str();
     catch (std::exception const& e) 
        VS_LOG_AND_FLUSH(fatal,
            "General Error determining savegame's last saved date/time: "s +
                e.what());
     catch (...) 
        VS_LOG_AND_FLUSH(fatal,
                         "Really bad error determining savegame's last saved "
                         "date/time! What just happened??");
    
    return "error"; // TODO?


int main() 
    std::cout << DescribeSaveGame("test.bin", true) << std::endl;
    std::cout << DescribeSaveGame("test.bin", false) << std::endl;

打印

Savegame: test.bin
_________________
Saved on: 2021-Sep-11 14:29:09

Savegame: test.bin
_________________
Saved on: 2021-Sep-11 14:29:09

最简单

以上暗示您可能可以将整个事情简化为 lexical_cast:

std::string DescribeSaveGame(std::string const& filename) 
    auto save_date = boost::posix_time::from_time_t(
        last_write_time(absolute(filename, getSaveDir())));

    return "Savegame: " + filename + lf + "_________________" + lf +
        "Saved on: " + boost::lexical_cast<std::string>(save_date) + lf;

Live On Coliru

仍然打印

Savegame: test.bin
_________________
Saved on: 2021-Sep-11 14:37:10

【讨论】:

旁注:如果您要依赖构面,还可以考虑使用boost::posix_time::time_facet 来控制不仅仅是日期部分。

以上是关于C++ - 如何以独立于平台、线程安全的方式以用户首选的日期/时间语言环境格式格式化文件的最后修改日期和时间的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 C++ 以跨平台方式找到用户的主目录?

以独立于平台的方式访问串口[关闭]

在安全且独立于平台的 Unity 中保存游戏数据的最佳方式

以编程方式查找机器上的内核数

Java

关于进制.