Действительно ли хранилище std :: chrono :: years по крайней мере 17 бит? - PullRequest
14 голосов
/ 13 марта 2020

От 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);
}

[ Годболт ссылка ]

Ответы [ 2 ]

8 голосов
/ 13 марта 2020

Статья cppreference правильная . Если libc ++ использует меньший тип, то это похоже на ошибку в libc ++.

4 голосов
/ 16 марта 2020

Я разбиваю пример на 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. Это средняя длина гражданского года.

Подвыражение days{365} представляет собой duration с period, равным ratio<86'400> и значением, равным 365.

Подвыражение years{0} + days{365} представляет собой duration с period, равным ratio<216> и значением, равным 146'000. Это формируется путем нахождения сначала common_type_t из ratio<31'556'952> и ratio<86'400>, который представляет собой GCD (31'556'952, 86'400) или 216. Библиотека сначала преобразует оба операнда в эту общую единицу, а затем выполняет сложение в общей единице.

Чтобы преобразовать years{0} в единицы с периодом 216 с, нужно умножить 0 на 146'097. Это очень важный момент. Это преобразование может легко вызвать переполнение, когда выполняется только с 32 битами.

Далее мы возьмем наш 146000[216]s duration и преобразовать его в длительность с period из ratio<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 UT C, исключая високосные секунды.

Наконец, 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_MAX (2'147'483'647), и что базовые rep для years и 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, которые добавляют years и days и используют значение для years, превышающее 14699, испытывают переполнение int.

Если кто-то действительно хочет это сделать хронологические вычисления с years и days таким образом, тогда было бы разумно использовать 64-битную арифметику c. Это может быть достигнуто путем преобразования years в единицы с rep, используя более 32 бит в начале вычисления. Например:

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

Если добавить 0s к years, (seconds должно содержать не менее 35 бит), то для common_type rep в первый раз вводится значение 64 бит сложение (years{14700} + 0s) и продолжается в 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 никогда не создается. И мы никогда даже не приблизимся к пределу для years, days или year.

Хотя, если вы ожидали 16700-01-01, то у вас все еще есть проблема, и способ ее исправить вместо этого сделать календарный расчет:

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