C++11替代localtime_r的方法

40
C++中使用strftime函数定义时间格式,需要一个“分解时间”记录struct tm。然而,C和C++03语言没有提供线程安全的方法来获取此类记录;整个程序只有一个主struct tm
在C++03中,这种情况还算可以接受,因为该语言不支持多线程;它仅支持支持多线程的平台,这些平台提供了像POSIX localtime_r这样的工具。
C++11还定义了新的时间工具,与非分解的time_t类型进行交互,这正是用于重新初始化全局struct tm的类型。但是,获取time_t并不是问题所在。
我是否遗漏了某些东西,或者这个任务仍然需要依赖于POSIX?
编辑:以下是一些解决方法的代码。它保持与提供::localtime_r的多线程环境兼容,并且在只提供std::localtime的单线程环境下也可以使用。它也可以轻松地适应其他函数的检查,例如posix::localtime_r::localtime_s等等。
namespace query {
    char localtime_r( ... );

    struct has_localtime_r
        { enum { value = sizeof localtime_r( std::declval< std::time_t * >(), std::declval< std::tm * >() )
                        == sizeof( std::tm * ) }; };


    template< bool available > struct safest_localtime {
        static std::tm *call( std::time_t const *t, std::tm *r )
            { return localtime_r( t, r ); }
    };

    template<> struct safest_localtime< false > {
        static std::tm *call( std::time_t const *t, std::tm *r )
            { return std::localtime( t ); }
    };
}
std::tm *localtime( std::time_t const *t, std::tm *r )
    { return query::safest_localtime< query::has_localtime_r::value >().call( t, r ); }

你能不能把localtime放在一个使用互斥锁或其他轻量级锁的函数中?我不认为这里需要使用POSIX。 - Nicol Bolas
9
@Nicol:是的,但只要没有其他人尝试做同样的事情,这才是安全的。换句话说,在程序中可以运行,在库中就不行。 - Potatoswatter
但是,如果他们从线程化代码中调用非线程安全函数,那么他们将会得到他们应得的结果。你可以为他们提供一个可用的函数,如果他们使用标准的C函数,则其行为未定义。 - Nicol Bolas
2
@Nicol:什么?我正在编写一个库,无法向其用户提供这样的限制。 - Potatoswatter
你不能告诉用户不要在多线程代码中调用非线程安全的函数吗?或者说,线程是一项你不想向用户公开的实现细节吗? - Nicol Bolas
5
@Nicol:我的库不支持多线程,但我希望它能够与多线程代码兼容。你建议使用互斥锁保护并且仍然调用localtime的方法,这正是我建议告诉用户不要做的事情。我不能强制要求我的互斥锁成为localtime函数的唯一锁 - 库的功能与此无关。无论如何,任何涉及互斥锁的方案都会是错误的解决方案,因为我的代码不是多线程的,只是线程安全的。 - Potatoswatter
2个回答

33

你没有错过什么。

下一个C标准(预计于今年发布)在附录K中确实有定义:

struct tm *localtime_s(const time_t * restrict timer,
                       struct tm * restrict result);

而且这个新函数是线程安全的!但别太高兴了,因为有两个主要问题:

  1. localtime_s 是 C11 的一个可选扩展。

  2. C++11 引用了 C99,而不是 C11。无论是可选的还是不可选的,都找不到local_time_s

更新

自从我回答这个问题以来已经过去了4年,我也对这个领域中C ++工具的设计感到沮丧。我受到启发,创建了现代C++工具来处理这个问题:

http://howardhinnant.github.io/date/tz.html

#include "tz.h"
#include <iostream>

int
main()
{
    using namespace date;
    auto local_time = make_zoned(current_zone(), std::chrono::system_clock::now());
    std::cout << local_time << '\n';
}

这是我的输出结果:

2015年10月28日14:17:31.980135 EDT

local_time 是一个由 std::chrono::system_clock::time_pointtime_zone 组成的对,表示本地时间。

有一些实用程序可将 std::chrono::system_clock::time_point 分解为人类可读的字段类型,例如年、月、日、小时、分钟、秒和亚秒。以下是重点介绍这些(非时区)部分的演示视频:

https://www.youtube.com/watch?v=tzyGjOm8AKo

所有这些当然都是线程安全的(这是现代C++)。

更新2

上述内容现在已成为 C++20 的一部分,具有略微不同的语法:

#include <chrono>
#include <iostream>

int
main()
{
    namespace chr = std::chrono;

    chr::zoned_time local_time{chr::current_zone(), chr::system_clock::now()};
    std::cout << local_time << '\n';
}

他们改变了名字!感谢提供信息。无论如何,可以查询localtime_r是否可用,否则可以回退到localtime。看来我最终能实现我所期望的。 - Potatoswatter

1
以下答案指出了mktime()的一个关键细节(即在C99之前,它是处理struct tm的唯一线程安全函数):

这里有一个可移植的替代方案,可以替代localtime_rlocaltime_s

//  localtime_s(&startTm, &startTimeT);
tm startTm{};
startTm.tm_isdst = -1;
startTm.tm_year = 122;
auto refTimeT = mktime(&startTm);
startTm.tm_sec += startTimeT - refTimeT;
mktime(&startTm);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接