structtm是否将时区信息存储为其数据成员



考虑以下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中的是一个明确的整数值,而localgm是存储人类可读日期/时间信息的struct tm。然后,我打印出仅基于struct tm对象的格式化信息(时区)。

根据cplusplus参考,struct tm的数据成员是

tm_sec  
tm_min  
tm_hour 
tm_mday 
tm_mon  
tm_year 
tm_wday 
tm_yday 
tm_isdst

如果这就是struct tm所包含的全部内容,那么程序如何从中知道时区信息?也就是说,它如何知道时区对于localHKT,而对于gmUTC

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

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

C标准在7.27.1中规定了时间的组成部分:

tm结构应包含至少以下成员任何订单。成员及其正常范围的语义为在评论中表达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被区域设置的时区名称或缩写或如果没有可确定的时区,则不使用字符[tm_isdst]

也就是说,供应商可以举手简单地说:"没有时区是可确定的,所以我根本不输出任何字符。">

我的观点:C计时API是一团糟。


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

C++20规范将其从"无字符"更改为在time_zone缩写不可用时抛出的异常:

http://eel.is/c++草稿/时间。格式#3

除非明确要求,否则格式化时间类型的结果不包含时区缩写和时区偏移信息如果信息可用,则转换说明符CCD_ 24和CCD_。[注意:如果信息不可用,并且%Z%z转换说明符出现在时间格式规范中,这是一个异常类型为format_­error的类型被抛出--尾注]

除了上面的段落不是描述C的strftime,而是一个新的format函数,它操作于std::chrono类型,而不是tm。此外,还有一种新的类型:std::chrono::zoned_time(http://eel.is/c++draft/time.zone.zonedtime),总是具有可用的time_zone缩写(和偏移量),并且可以使用上述format函数进行格式化。

示例代码:

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

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

如果你想试用这个库的预览版,它是免费的,在这里是开源的:https://github.com/HowardHinnant/date

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

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

预览库可以与C++11或更高版本一起使用。

zoned_time是在指定时间点精度的std::chrono::duration上模板化的,并且在上面的示例代码中使用C++17的CTAD特性推导出。如果你在C++11或C++14中使用这个预览库,语法看起来更像:

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

或者有一个非建议的标准化助手工厂功能,它将为您做推导:

cout << format("%Zn", make_zoned(current_zone(), now));

(#CTAD_elimites_factory_functions)

感谢您对这个问题的所有评论,这些评论有助于指明正确的方向。我在下面发布了一些我自己的研究。我的发言基于我在GitHub上发现的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也不例外。(正如霍华德在回答中提到的,C++正在努力做得更好。)

尽管每个人都知道全局变量是坏的,但C的日期/时间函数基本上使用了其中的几个。实际上,"本系统当前时区"的概念是一个全局变量,描述该时区的全局数据在localtimestrftime以及许多其他函数之间随意共享。

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

这显然是一种次优的安排,如果有一种方法让程序动态地更改其想要用于localtime和其他的时区,这将开始引发真正的问题。(这种安排之所以持续存在,部分原因是而不是实际上是程序更改其使用的本地时区的一种好的、可移植的标准方法。)

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

您可以在本文中阅读更多关于C中日期/时间支持的肮脏历史:Eric Raymond的《C中的时间、时钟和日历编程》。

最新更新