std :: chrono :: years 스토리지는 실제로 최소 17 비트입니까? throw format_error(“invalid format”);

에서 cppreference

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

사용하여 libc++, 그것의 밑줄 저장 보인다 std::chrono::yearsIS 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 링크 ]



답변

cppreference 기사가 정확합니다 . 만약 libc ++가 더 작은 타입을 사용한다면 이것은 libc ++의 버그 인 것 같습니다.


답변

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는 durationA의 period동일 ratio<31'556'952>및 동일 값 0. 참고 years{1}부동 소수점 표현을 days정확히 365.2425이다. 이것은 민간 연도 의 평균 길이입니다.

서브 표현식 days{365}A는 durationA의 period동일 ratio<86'400>및 동일 값 365.

서브 표현식 years{0} + days{365}A는 durationA의 period동일 ratio<216>및 동일 값 146'000. 이것은 처음 발견함으로써 형성된다 common_type_tratio<31'556'952>ratio<86'400>최대 공약수 (31’556’952, 86’400) 또는 (216)이 공통 유닛에 제 라이브러리 두 피연산자 변환되고, 그 후 공통 부에 첨가한다.

years{0}주기가 216 초인 단위 로 변환하려면 0에 146’097을 곱해야합니다. 이것은 매우 중요한 포인트입니다. 이 변환은 32 비트 만 있으면 오버플로를 쉽게 일으킬 수 있습니다.

<옆으로>

이 시점에서 혼란을 느낄 경우, 코드가 가능성이 의도 때문이다 역법 계산을하지만, 실제로하고있는 연대 기적 계산을. 달력 계산은 달력을 사용한 계산입니다.

달력은 일과 관련하여 물리적 길이가 다른 달과 년과 같은 모든 종류의 불규칙성을 가지고 있습니다. 캘린더 계산은 이러한 불규칙성을 고려합니다.

시간순 계산은 고정 단위로 작동하며 달력에 관계없이 숫자를 계산합니다. 연대기 계산은 그레고리력, 율리우스 력, 힌두 력, 중국 력 등을 사용하더라도 상관 없습니다.

</ aside>

우리는 우리의 걸릴 다음으로 146000[216]s기간을하고있는 기간으로 변환 periodratio<86'400>(라는 이름의 타입 별칭이있다 days). 함수 floor<days>()는이 변환을 수행하고 결과는 365[86400]s간단히 말하면 365d됩니다.

다음 단계는를 가져 와서 duration로 변환합니다 time_point. 의 유형은 time_point이다 time_point<system_clock, days>라는 형식 별칭을 가지고있는 sys_days. 이것은 신기원 days이후의 숫자이며 system_clock, 윤초를 제외한 1970-01-01 00:00:00 UTC입니다.

마지막으로 값을 가진 sys_days로 변환됩니다 .year_month_day1971-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_MAX( 2'147'483'647) 및 기본 것을 rep모두의 years하고 days있다 int.

실제로 years{14700}단위 로 변환 [216]s하면 overflow : -2'147'341'396[216]s.

이 문제를 해결하려면 금전 계산으로 전환하십시오.

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

의 모든 결과 https://godbolt.org/z/SNivyp 추가 yearsdays및 값을 사용하여 years그 발생 14,699보다 큰 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

그러나 (이 범위에서) 중간 오버 플로우를 방지하는 또 다른 방법은 잘라야하는 것입니다 yearsdays정밀 전에 더 추가 days:

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

j값이 16669-12-31있습니다. 이제 [216]s장치가 처음부터 생성되지 않기 때문에 문제 가 발생하지 않습니다. 그리고 years, days또는 의 한계에 결코 도달하지도 않습니다 year.

을 기대하고 있지만 16700-01-01여전히 문제가 있으며이를 해결하는 방법은 대신에 계산 계산을 수행하는 것입니다.

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