struct tm 是否将时区信息存储为它的数据成员

Does struct tm store time zone information as its data member

考虑以下 C++ 代码

#include <ctime>
#include <iostream>

int main()
{
    std::time_t now = std::time(nullptr);
    struct tm local = *std::localtime(&now);
    struct tm gm = *std::gmtime(&now);
    char str[20];
    std::strftime(str, 20, "%Z", &local);
    std::cout << str << std::endl;          // HKT
    std::strftime(str, 20, "%Z", &gm);
    std::cout << str << std::endl;          // UTC

    return 0;
}

所以在now中存储的是一个明确的整数值,而localgmstruct tm存储人类可读的date/time信息。然后我打印出仅基于 struct tm 对象的格式化信息(时区)。

根据cplusplus referencestruct tm的数据成员是

tm_sec  
tm_min  
tm_hour 
tm_mday 
tm_mon  
tm_year 
tm_wday 
tm_yday 
tm_isdst

如果这就是 struct tm 包含的全部内容,程序如何知道其中的时区信息?也就是说,它怎么知道local的时区是HKTgm的时区是UTC

如果 struct tm 包含的内容还不止这些,请解释它如何存储时区信息。

顺便说一句,虽然演示代码是用 C++ 编写的,但我想这个问题本质上也是一个合法的 C 问题。

感谢您对问题的所有评论,这些评论有助于指明正确的方向。我post下面是我自己的一些研究。我的发言基于我在 GitHub 上找到的 archived repo GNU C 库。它的版本是 2.28.9000.

glibc/time/bits/types/struct_tm.h中有

struct tm
{
  int tm_sec;           /* Seconds. [0-60] (1 leap second) */
  int tm_min;           /* Minutes. [0-59] */
  int tm_hour;          /* Hours.   [0-23] */
  int tm_mday;          /* Day.     [1-31] */
  int tm_mon;           /* Month.   [0-11] */
  int tm_year;          /* Year - 1900.  */
  int tm_wday;          /* Day of week. [0-6] */
  int tm_yday;          /* Days in year.[0-365] */
  int tm_isdst;         /* DST.     [-1/0/1]*/

# ifdef __USE_MISC
  long int tm_gmtoff;       /* Seconds east of UTC.  */
  const char *tm_zone;      /* Timezone abbreviation.  */
# else
  long int __tm_gmtoff;     /* Seconds east of UTC.  */
  const char *__tm_zone;    /* Timezone abbreviation.  */
# endif
};

似乎 struct tm 确实存储了时区信息,至少在这个实现中是这样。

C 标准在 7.27.1 时间的组成部分中说:

The tm structure shall contain at least the following members, in any order. The semantics of the members and their normal ranges are expressed in the comments.318)

int tm_sec;    // seconds after the minute — [0, 60]
int tm_min;    // minutes after the hour — [0, 59]
int tm_hour;   // hours since midnight — [0, 23]
int tm_mday;   // day of the month — [1, 31]
int tm_mon;    // months since January — [0, 11]
int tm_year;   // years since 1900
int tm_wday;   // days since Sunday — [0, 6]
int tm_yday;   // days since January 1 — [0, 365]
int tm_isdst;  // Daylight Saving Time flag

(重点是我的)

也就是说,允许实现向 tm 添加其他成员,正如您在 glibc/time/bits/types/struct_tm.h 中发现的那样。 POSIX 规范的措辞几乎相同。

结果是%Z(甚至%z)在strftime中不能被认为是可移植的。 %Z 的规范反映了这一点:

%Z is replaced by the locale’s time zone name or abbreviation, or by no characters if no time zone is determinable. [tm_isdst]

也就是说,允许供应商举手简单地说:"no time zone was determinable, so I'm not outputting any characters at all."

我的看法:C 时序 API 一团糟。


我正尝试在 <chrono> 库中针对即将推出的 C++20 标准进行改进。

C++20 规范将此从 "no characters" 更改为如果 time_zone 缩写不可用则抛出异常:

http://eel.is/c++draft/time.format#3

Unless explicitly requested, the result of formatting a chrono type does not contain time zone abbreviation and time zone offset information. If the information is available, the conversion specifiers %Z and %z will format this information (respectively). [ Note: If the information is not available and a %Z or %z conversion specifier appears in the chrono-format-spec, an exception of type format_­error is thrown, as described above. — end note ]

除了上面的段落没有描述 C 的 strftime,而是一个新的 format 函数,它在 std::chrono 类型上运行,而不是 tm。此外还有一个新类型:std::chrono::zoned_time (http://eel.is/c++draft/time.zone.zonedtime) always 有可用的 time_zone 缩写(和偏移量)并且可以用前面提到 format 函数。

示例代码:

#include <chrono>
#include <iostream>

int
main()
{
    using namespace std;
    using namespace std::chrono;
    auto now = system_clock::now();
    std::cout << format("%Z\n", zoned_time{current_zone(), now});   // HKT (or whatever)
    std::cout << format("%Z\n", zoned_time{"Asia/Hong_Kong", now}); // HKT or HKST
    std::cout << format("%Z\n", zoned_time{"Etc/UTC", now});        // UTC
    std::cout << format("%Z\n", now);                               // UTC
}

(免责声明:format 函数中格式化字符串的最终语法可能会略有不同,但功能会在那里。)

如果您想试用此库的预览版,可在此处免费开源:https://github.com/HowardHinnant/date

需要一些安装:https://howardhinnant.github.io/date/tz.html#Installation

在此预览中,您将需要使用 header "date/tz.h",库中的内容在 namespace date 而不是 namespace std::chrono

预览库可用于 C++11 或更高版本。

zoned_time 以指定时间点精度的 std::chrono::duration 为模板,并在上面的示例代码中使用 C++17's CTAD feature 推导出来。如果您在 C++11 或 C++14 中使用此预览库,则语法看起来更像:

cout << format("%Z\n", zoned_time<system_clock::duration>{current_zone(), now});

或者有一个 non-proposed-for-standardization 辅助工厂函数可以为您进行推导:

cout << format("%Z\n", make_zoned(current_zone(), now));

(#CTAD_eliminates_factory_functions)

日期和时间编程如此困难的原因之一是它从根本上说至少是一个有点困难的问题:"Thirty days hath September"、sexagesimal arithmetic、时区、夏令时,以及闰年,更不用说闰秒了。

但另一个困难的原因是太多的库和语言把它搞得一团糟,不幸的是,C 也不例外。 (正如霍华德在他的回答中提到的那样,C++ 正在努力做得更好。)

尽管每个人都知道全局变量不好,但 C 的 date/time 函数基本上使用其中的几个。实际上,"this system's current time zone" 的概念是一个全局变量,描述时区的全局数据在 localtimestrftime 以及许多其他函数之间随意共享。

因此 strftime 可以根据该全局数据填写 %z%Z,即使它不是作为 struct tm 值的一部分传入的。

这显然不是最佳安排,如果有一种方法可以让程序动态地更改它想要用于 [=10] 的时区,它就会开始引起真正的问题=] 和其余的。 (这种安排之所以持续存在,部分原因是 不是 实际上是一个很好的、可移植的、标准的程序更改它正在使用的本地时区的方法。)

多年来,一直有各种半心半意的尝试来清理一些混乱(当然,同时保持向后兼容性)。其中一项尝试涉及您在某些系统版本的 struct tm 中发现的扩展 tm_gmtofftm_zone 字段。这些添加是 巨大的 改进——我无法想象在没有它们的情况下在系统上进行严肃的 date/time 编程——但它们仍然不是标准的,而且仍然有许多系统没有它们(甚至 "hidden" 拼写 __tm_gmtoff__tm_zone)。

您可以在这篇论文中阅读更多关于 date/time C 支持的肮脏历史:Time, Clock, and Calendar Programming In C,Eric Raymond。