std :: chrono :: years的存储空间真的至少是17位吗?


14

来自cppreference

std::chrono::years (since C++20) duration</*signed integer type of at least 17 bits*/, std::ratio<31556952>>

使用libc++,它似乎的强调存储std::chrono::years就是short其中签订16位

std::chrono::years( 30797 )        // yields  32767/01/01
std::chrono::years( 30797 ) + 365d // yields -32768/01/01 apparently UB

cppreference上有错别字吗?

例:

#include <fmt/format.h>
#include <chrono>

template <>
struct fmt::formatter<std::chrono::year_month_day> {
  char presentation = 'F';

  constexpr auto parse(format_parse_context& ctx) {
    auto it = ctx.begin(), end = ctx.end();
    if (it != end && *it == 'F') presentation = *it++;

#   ifdef __exception
    if (it != end && *it != '}') {
      throw format_error("invalid format");
    }
#   endif

    return it;
  }

  template <typename FormatContext>
  auto format(const std::chrono::year_month_day& ymd, FormatContext& ctx) {
    int year(ymd.year() );
    unsigned month(ymd.month() );
    unsigned day(ymd.day() );
    return format_to(
        ctx.out(),
        "{:#6}/{:#02}/{:#02}",
        year, month, day);
  }
};

using days = std::chrono::duration<int32_t, std::ratio<86400> >;
using sys_day = std::chrono::time_point<std::chrono::system_clock, std::chrono::duration<int32_t, std::ratio<86400> >>;

template<typename D>
using sys_time = std::chrono::time_point<std::chrono::system_clock, D>;
using sys_day2 = sys_time<days>;

int main()
{
  auto a = std::chrono::year_month_day( 
    sys_day( 
      std::chrono::floor<days>(
        std::chrono::hours( (1<<23) - 1 ) 
      )
    )
  );

  auto b = std::chrono::year_month_day( 
    sys_day( 
      std::chrono::floor<days>(
        std::chrono::minutes( (1l<<29) - 1 ) 
      )
    )
  );

  auto c = std::chrono::year_month_day( 
    sys_day( 
      std::chrono::floor<days>(
        std::chrono::seconds( (1l<<35) - 1 ) 
      )
    )
  );

  auto e = std::chrono::year_month_day( 
    sys_day( 
      std::chrono::floor<days>(
        std::chrono::days( (1<<25) - 1 ) 
      )
    )
  );

  auto f = std::chrono::year_month_day( 
    sys_day( 
      std::chrono::floor<days>(
        std::chrono::weeks( (1<<22) - 1 ) 
      )
    )
  );

  auto g = std::chrono::year_month_day( 
    sys_day( 
      std::chrono::floor<days>(
        std::chrono::months( (1<<20) - 1 ) 
      )
    )
  );

  auto h = std::chrono::year_month_day( 
    sys_day( 
      std::chrono::floor<days>(
        std::chrono::years( 30797 ) // 0x7FFF - 1970
      )
    )
  );

  auto i = std::chrono::year_month_day( 
    sys_day( 
      std::chrono::floor<days>(
        std::chrono::years( 30797 ) // 0x7FFF - 1970
      ) + std::chrono::days(365)
    )
  );

  fmt::print("Calendar limit by duration's underlining storage:\n"
             "23 bit hour       : {:F}\n"
             "29 bit minute     : {:F}\n"
             "35 bit second     : {:F}\n"
             "25 bit days       : {:F}\n"
             "22 bit week       : {:F}\n"
             "20 bit month      : {:F}\n"
             "16? bit year      : {:F}\n"
             "16? bit year+365d : {:F}\n"
             , a, b, c, e, f, g, h, i);
}

[ Godbolt链接 ]


2
year范围:eel.is/c++draft/time.cal.year#members-19 years范围: eel.is/c++draft/time.synyear是公民年度的“名称”,需要16位。 years是一个时间长度,与时间不同year。一个可以减去两个year,结果为type yearsyears必须能够保存的结果year::max() - year::min()
霍华德·南南

1
std::chrono::years( 30797 ) + 365d无法编译。
霍华德·南南

1
结果years{30797} + days{365}为204528013,单位为216s。
霍华德·南南

1
那只是增加了两个持续时间。禁止它意味着禁止hours{2} + seconds{5}
霍华德·辛南特

4
我的猜测是,您将日历组件与持续时间类型混淆了,因为它们确实具有类似的名称。这里有一个一般的规则: duration名称是复数: yearsmonthsdays。历法的部件名称是单数: yearmonthdayyear{30797} + day{365}是编译时错误。 year{2020}是今年。 years{2020}持续时间为2020年。
霍华德·南南

Answers:


8

cppreference文章是正确的。如果libc ++使用较小的类型,则这似乎是libc ++中的错误。


但是添加另一个word可能很少使用的方法是否会year_month_day不必要地增加向量的数量呢?难道at least 17 bits不算作规范文本吗?
桑索

3
@sandthorn year_month_day包含year,不包含yearsyear尽管类型short用作说明,但的表示形式不必为16位。OTOH,years定义中的17位部分是规范性的,因为它不仅标记为博览会。坦率地说,说它至少包含17位,然后不要求它是没有意义的。
Andrey Semashev

1
yearyear_month_day似乎int确实如此。=> operator int我认为这支持at least 17 bits years实现。
桑索

您介意编辑答案吗?原来std :: chrono :: years实际上是int而std :: chrono :: year的最大值是任意的32767。–
Sandthorn

@sandthorn答案是正确的,我不明白为什么我需要对其进行编辑。
Andrey Semashev

4

我正在https://godbolt.org/z/SNivyp逐个细分示例:

  auto a = std::chrono::year_month_day( 
    sys_days( 
      std::chrono::floor<days>(
        std::chrono::years(0) 
        + std::chrono::days( 365 )
      )
    )
  );

简化和假设using namespace std::chrono范围:

year_month_day a = sys_days{floor<days>(years{0} + days{365})};

子表达式years{0}是a durationperiod等于ratio<31'556'952>和等于0。请注意years{1},以浮点表示days,正好是365.2425。这是公民年度的平均时长。

子表达式days{365}是a durationperiod等于ratio<86'400>和等于365

子表达式years{0} + days{365}是a durationperiod等于ratio<216>和等于146'000。这是通过首先发现所形成common_type_tratio<31'556'952>并且ratio<86'400>其是GCD(31'556'952,86'400),或216这两个操作数的库第一转换到这个共同的单元,然后执行在公共单元中的加法。

要转换years{0}为周期为216s的单位,必须将0乘以146'097。这恰好是非常重要的一点。仅使用32位完成此转换时,很容易导致溢出。

<aside>

如果此时您感到困惑,那是因为该代码可能打算进行日历计算,但实际上是在按时间顺序进行计算。日历计算是带有日历的计算。

日历具有各种不规则性,例如月份和年份的天数不同。日历计算将这些不规则性考虑在内。

按时间顺序的计算使用固定单位,并且只计算出数字而不考虑日历。如果您使用公历,朱利安历法,印度教历法,中国历法等,则按时间顺序计算并不重要。

</ aside>

接下来,我们把我们的146000[216]s时间并将其转换为具有持续时间periodratio<86'400>(其中有一个名为类型的别名days)。该函数floor<days>()执行此转换,结果为365[86400]s或更简单地为just 365d

下一步duration将转换为time_point。该类型的time_pointtime_point<system_clock, days>它有一个名为类型的别名sys_days。这只是dayssystem_clock1970年1月1日00:00:00 UTC 以来的一个计数,不包括leap秒。

最后,sys_days将转换为year_month_day具有的值1971-01-01

一种简单的计算方法是:

year_month_day a = sys_days{} + days{365};

考虑类似的计算:

year_month_day j = sys_days{floor<days>(years{14699} + days{0})};

结果为日期16668-12-31。这可能比您预期的早了一天((14699 + 1970)-01-01)。years{14699} + days{0}现在,子表达式为: 2'147'479'803[216]s。需要注意的是运行时间值已接近INT_MAX2'147'483'647),并且底层rep两者的yearsdaysint

的确,如果转换years{14700}为单位,[216]s则会溢出: -2'147'341'396[216]s

要解决此问题,请切换到日历计算:

year_month_day j = (1970y + years{14700})/1/1;

所有的结果https://godbolt.org/z/SNivyp被添加yearsdays使用一个值years大于14699遇到int溢出。

如果一个人真的想要做时间的计算与yearsdays这样的话,那将是明智的使用64位运算。这可以通过在计算的早期使用大于32位的转换years为单位来实现rep。例如:

years{14700} + 0s + days{0}

通过添加0syears,(seconds必须有至少35个比特),则common_type rep被迫64个比特用于第一加法(years{14700} + 0s),并继续在添加时是64位days{0}

463'887'194'400s == 14700 * 365.2425 * 86400

避免中间溢出(在此范围内)的另一种方法是添加更多内容之前将其截断yearsdays精度:days

year_month_day j = sys_days{floor<days>(years{14700})} + days{0};

j具有价值16669-12-31。这样可以避免此问题,因为现在[216]s根本就不会创建该单元。我们甚至从来没有接近了极限yearsdaysyear

尽管如果您期望16700-01-01,那么您仍然有问题,而更正此问题的方法是进行日历计算:

year_month_day j = (1970y + years{14700})/1/1;

1
很好的解释。我担心按时间顺序计算。如果我years{14700} + 0s + days{0}在代码库中看到的话,我将不知道0s在那里正在做什么以及它有多重要。有替代的,也许更明确的方法吗?会duration_cast<seconds>(years{14700}) + days{0}更好吗?
bolov

duration_cast会更糟,因为duration_cast用于非截断转换的格式不正确。截断转换可能是逻辑错误的来源,最好仅在需要时使用“大锤子”,以便您可以轻松地在代码中发现截断转换。
霍华德·辛南特

1
可以创建一个自定义的持续时间: use llyears = duration<long long, years::period>;,然后使用它。但最好的办法可能是考虑您要完成的工作,并质疑您是否正以正确的方式进行操作。例如,您真的需要一万年的时间精度吗?民用日历仅精确到4000年中的1天左右。也许浮点数千年将是更好的单位?
霍华德·辛南特

澄清:chrono对民用日历的建模精确在-32767/1/1至32767/12/31的范围内。相对于太阳系建模,民用日历的准确性在4000年中只有1天左右。
霍华德·辛南特

1
这真的取决于用例,而我目前很难想到一个激励性的用例来添加yearsdays。这实际上是将365.2425天的整数倍添加到某些整数天中。通常,如果您要按月或数年的时间顺序进行计算,则是对一些物理或生物学建模。也许这篇文章以不同的方式添加monthssystem_clock::time_point有助于澄清两种类型的计算之间的区别:stackoverflow.com/a/43018120/576911
霍华德Hinnant(欣南特)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.