std :: chrono :: years的存储确实至少有17位吗?

问题描述 投票:12回答:2

来自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 link]

c++ chrono c++20 libc++
2个回答
8
投票

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


2
投票

[我正在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}duration,其中period等于ratio<31'556'952>,并且值等于0。请注意,表示为浮点years{1}days恰好是365.2425。这是公民年度的[[average长度。

子表达式days{365}duration,其中period等于ratio<86'400>,并且值等于365

子表达式years{0} + days{365}duration,其中period等于ratio<216>,并且值等于146'000。这是通过首先找到LCM(31'556'952,86'400)或216的common_type_tratio<31'556'952>ratio<86'400>来形成的。库首先将两个操作数都转换为该公共单位,然后以通用单位进行加法。

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

如果在这一点上您感到困惑,那是因为该代码可能打算进行

calendrical

计算,但实际上是在进行chronological计算。日历计算是带有日历的计算。 日历具有各种不规则性,例如月份和年份在天数方面具有不同的物理长度。日历计算将这些不规则性考虑在内。

按时间顺序进行的计算使用固定单位,并且仅计算出数字而不考虑日历。如果您使用公历,儒略历,印度历,中国历等,按时间顺序计算并不重要。

aside>

接下来,我们将146000[216]s的持续时间转换为periodratio<86'400>的持续时间(其别名为days)。函数floor<days>()执行此转换,结果为365[86400]s,或更简单地说,仅为365d

下一步将duration转换为time_pointtime_point的类型为time_point<system_clock, days>,其别名为sys_days。从days纪元(1970年1月1日00:00:00 UTC)开始,这只是system_clock的计数,不包括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),并且repyears的基础days均为int

事实上,如果将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}

[通过将0s加到years,(seconds必须至少具有35位),然后对于第一次加法[common_type),rep years{14700} + 0s被强制为64位,并继续以64添加days{0}时的位:

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

另一种避免中间溢出(在此范围内)的方法是将years截断为days精度

之前

添加更多的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;

© www.soinside.com 2019 - 2024. All rights reserved.