【问题标题】:How safe is it to assume time_t is in seconds?假设 time_t 以秒为单位有多安全?
【发布时间】:2013-10-28 07:01:56
【问题描述】:

我正在对时间进行大量计算,通过添加秒来构建相对于其他时间对象的时间对象。该代码应该在嵌入式设备和服务器上运行。大多数文档都说time_t 是某种算术类型,通常存储自纪元以来的时间。假设time_t 存储了几秒钟之后有多安全?如果我们可以假设,那么我们可以只使用加法和减法而不是localtimemktimedifftime

到目前为止,我已经通过使用constexpr bool time_tUsesSeconds 解决了这个问题,表示假设time_t 使用秒是否安全。如果假设 time_t 以秒为单位是不可移植的,有没有办法自动初始化该常量?

time_t timeByAddingSeconds(time_t theTime, int timeIntervalSeconds) {
    if (Time_tUsesSeconds){
        return theTime + timeIntervalSeconds;
    } else {
        tm timeComponents = *localtime(&theTime);
        timeComponents.tm_sec += timeIntervalSeconds;
        return mktime(&timeComponents);
    }
}

【问题讨论】:

  • time_t 的单位由设置它的函数决定,而不是由类型本身决定。因此,如果您使用的功能说它提供“秒”,那么您可以放心,这就是您所得到的。

标签: c++ date time time-t


【解决方案1】:

POSIX specification 说明了它以秒为单位的事实,因此,如果您正在为符合 POSIX 的环境进行编码,则可以依赖它。

C++ 标准还规定time_t 必须是算术类型。

无论如何,Unix 计时系统(自纪元以来的第二个)将在 2038 年溢出。因此,在此日期之前,C++ 实现很可能会切换到其他非 int 数据类型(64 位int 或更复杂的数据类型)。无论如何,切换到 64 位 int 会破坏与以前代码的二进制兼容性(因为它需要更大的变量),并且应该重新编译所有内容。使用 32 位不透明句柄不会破坏二进制兼容性,您可以更改底层库,一切仍然有效,但 time_t 不再以秒为单位,它将成为以秒为单位的时间数组的索引。因此,建议您使用您提到的函数来操作time_t 值,并且不要假设time_t 上的任何内容。

【讨论】:

  • 实际上,除了算术类型(在 POSIX 系统上是强制性的)之外,没有人会实现time_t。当前的 64 位类 UNIX 系统已经将其实现为 64 位整数(因为 64 位架构的 ABI 更改已经强制重新编译)和the same holds on Windows。任何其他解决方案必然会造成更大的创伤,因为即使在非 POSIX 系统上,将time_t 视为“自纪元以来的秒数”也是司空见惯的,并且更改其语义会默默地破坏很多东西。
  • @MatteoItalia - C 和 C++ 标准都要求 time_t 是算术类型。
  • 解决 y2038 问题的实际实现是在 32 位 Linux 和 glibc 中添加 *time64 系统调用和将 time_t 更改为 64 位。该计数也是一个整数时间,与以前完全相同,类似于 64 位 Linux,只是一个更宽的 time_t 值。见64-bit time_t in Linux Kernel
【解决方案2】:

如果 C++11 可用,您可以使用std::chrono::system_clockto_time_tfrom_time_tstd::chrono::time_point 相互转换,并使用chrono 的算术运算符。

如果您的计算涉及公历,您可以使用 HowardHinnant/date 库,或 C++20 的新日历工具(它们有 essentially the same API)。

【讨论】:

    【解决方案3】:

    对于time_t 表示的单位,标准 C 或标准 C++ 中没有要求。要以便携式方式使用秒,您需要使用struct tm。您可以使用mktimelocaltimetime_tstruct tm 之间进行转换。

    【讨论】:

    • 这是不正确的。正如 Giulio 在下面指出的那样,POSIX 将 time() (以及它的返回类型)定义为自纪元以来返回的秒数。显然,有可能有一个非 POSIX 系统,它的 typedef 具有相同名称的不同解释,但我不认为这就是问题所在(反正不存在这样的系统)。
    • 嗯,POSIX 并没有定义整个世界。为了确保time_t 代表您在承诺满足 POSIX 要求的系统上必须的秒数。 C 和 C++ 标准都没有要求。我已经修改了我的答案以专门参考这些标准。
    • 你知道一个真实的系统time_t(当然最初在Unix中定义)的单位不是秒吗?我不是。它在任何地方都兼容,原因很明显。如果您也不这样做,我看不出进行该讨论的价值。您不妨提醒人们不要使用printf(),因为某些虚构系统将其定义为abort() 的同义词。
    • @AndyRoss - 将printf 定义为abort 同义词的系统不满足C 标准。 time_t 不使用秒数的系统会。我真的不理解这种坚持使用不需要便携的东西而不是便携的东西。
    【解决方案4】:

    不要确定time_t 是否以秒为单位,因为time_t 是一种算术类型,您可以计算一个代表一秒的time_t 值,然后使用它。 This answer I wrote before 解释了方法并有一些注意事项,这里有一些示例代码(bad_time() 是自定义异常类,这里):

    time_t get_sec_diff() {
        std::tm datum_day;
        datum_day.tm_sec = 0;
        datum_day.tm_min = 0;
        datum_day.tm_hour = 12;
        datum_day.tm_mday = 2;
        datum_day.tm_mon = 0;
        datum_day.tm_year = 30;
        datum_day.tm_isdst = -1;
    
        const time_t datum_time = mktime(&datum_day);
        if ( datum_time == -1 ) {
            throw bad_time();
        }
    
        datum_day.tm_sec += 1;
        const time_t next_sec_time = mktime(&datum_day);
        if ( next_sec_time == -1 ) {
            throw bad_time();
        }
    
        return (next_sec_time - datum_time);
    }
    

    您可以调用该函数一次并将值存储在 const 中,然后在需要 time_t 秒时使用它。我不认为它会在 constexpr 中工作。

    【讨论】:

    • 我有点喜欢它,但它确实假设 time_t 以秒为单位,直到某个常数。 IE。在任何两秒之间,存在相同的差异。
    • 嗯,这意味着time_t 可以以某种方式准确地表示整秒,是的。如果它不能,那么你只是运气不好试图在time_t 上执行这种每秒算术,所以如果是这样的话,你并没有真正失去任何东西。如果 time_t 恰好被实现为 double (我不知道有任何现代系统会这样做),那么您也会遇到正常的浮点精度问题,即尝试增加 2000 万秒可能会给奇怪的结果。您可以随时根据 struct tm 检查您的结果,除非您要避免这样做。
    • 作为高级检查,您可以设置两个足够远的struct tms。例如,将两个 struct tms 恰好相隔一年(不跨越闰年),然后将较早的一个变成 time_t,添加 get_sec_diff() * 60 * 60 * 24 * 365 到它,然后检查 localtime() 看你是否得到与后者匹配的struct tm。如果你这样做了,那么你应该很好,因为如果来自 get_sec_diff() 的返回不是正好一秒,那么你应该在几英里之外。
    • 话虽如此,当然,在struct tms 上增加秒数确实不是那么困难,所以这通常是一个更好的解决方案。请注意,不能保证故意溢出 struct tm 的成员 - mktime() 将使任何超出范围的值“强制到指示的范围”,但标准中没有任何内容说它不能只需在不更改其他成员的情况下截断它们(例如,将 tm_sec 设置为 70 不需要将 tm_min 提前 1)。
    • 由于闰秒,可能很难在每个实现中找到 365*60*60*24 秒长的年份。可能是一年前?
    【解决方案5】:

    我的两分钱:在 Windows 上,它以秒为单位随着时间的推移,但一秒增加到下一秒所需的时间通常是 18*54.925 毫秒,有时是 19*54.925。其原因在this post 中进行了解释。

    【讨论】:

    • 55 毫秒是旧的 DOS 时钟速率。 NT 分支中自 1993 年以来的 Windows 版本使用 64 滴答/秒、15.625 毫秒的默认时钟中断率。
    • 我的四岁 HP W7 笔记本电脑的时钟频率为 2GHz。当我使用我的方法测量它时,我得到了 1995-1997 MHz 的结果,开源产品也得到了这个结果。我会用 15.625 周期得到它吗?
    【解决方案6】:

    (回答自己的问题)

    一个答案表明,只要使用 posix,time_t 以秒为单位,time_t 上的算术应该可以工作。

    第二个答案计算每秒的 time_t,并在进行算术运算时将其用作一个因素。但是还是有一些关于time_t的假设。

    最后我决定可移植性更重要,我不希望我的代码在某些嵌入式设备上静默失败。所以我用了第三种方式。它涉及存储一个整数,表示自程序启动以来的时间。 IE。我定义

     const static time_t time0 = time(nullptr);
     static tm time0Components = *localtime(&time0);
    

    整个程序中使用的所有时间值都只是整数,表示自time0 以来的时间差(以秒为单位)。从time_t 到这个增量秒,我使用difftime。回到time_t,我使用这样的东西:

    time_t getTime_t(int timeDeltaSeconds) {
        tm components = time0Components;
        components.tm_sec += timeDeltaSeconds;
        return mktime(&components);
    }
    

    这种方法允许像+,- 这样的操作便宜,但回到time_t 是昂贵的。请注意,时间增量值仅对程序的当前运行有意义。另请注意,当时区更改时,必须更新 time0Components。

    【讨论】:

      猜你喜欢
      • 2015-08-31
      • 2011-11-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-06-23
      • 2020-07-01
      • 2012-03-26
      相关资源
      最近更新 更多