Действительно ли хранилище 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::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 link ]

Облепиховый
источник
2
yearдиапазон: eel.is/c++draft/time.cal.year#members-19 years диапазон: eel.is/c++draft/time.syn . yearявляется именем гражданского года и требует 16 бит. yearsэто длительность хроно, а не то же самое, что year. Можно вычесть два, yearи результат имеет тип years. yearsтребуется, чтобы иметь возможность удерживать результат year::max() - year::min().
Говард Хиннант
1
std::chrono::years( 30797 ) + 365dне компилируется
Говард Хиннант
1
Результат years{30797} + days{365}204528013 с единицами 216.
Говард Хиннант
1
Это просто две продолжительности добавляются. Запрещать это означало бы запрещать hours{2} + seconds{5}.
Говард Хиннант
4
Я предполагаю, что вы путаете календарные компоненты с типами продолжительности, потому что у них есть такие похожие имена. Вот общее правило: durationимена во множественном числе years, months, days. Календарные имена компонентов единичны: year, month, day. year{30797} + day{365}это ошибка времени компиляции year{2020}в этом году years{2020}это продолжительность 2020 года.
Говард Хиннант

Ответы:

8

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

Андрей Семашев
источник
Но добавление другого, wordкоторый, вероятно, едва использовался, не было бы ненужным year_month_dayвектором? Разве это at least 17 bitsне считается нормальным текстом?
песчаная терна
3
@sandthorn year_month_dayсодержит year, а не years. Представление yearне обязательно должно быть 16-битным, хотя тип shortиспользуется в качестве экспозиции. OTOH, 17-битная часть в yearsопределении является нормативной, поскольку она не помечена как только экспозиция. И, честно говоря, говорить, что это как минимум 17 бит, а затем не требует его, бессмысленно.
Андрей Семашев
1
А yearв , year_month_dayкажется, в intсамом деле. => оператор int Я думаю, что это поддерживает at least 17 bits yearsреализацию.
песчаная терна
Не могли бы вы отредактировать свой ответ? Оказывается, std :: chrono :: years на самом деле int, а std :: chrono :: year - макс, 32767, произвольно ..
песчаная шишка
@sandthorn Ответ правильный, я не понимаю, зачем мне нужно его редактировать.
Андрей Семашев
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}представляет собой 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>который является НОД (31'556'952, 86'400), или 216. Библиотека сначала преобразует оба операнда к этой общей единице, а затем делают добавление в общем блоке.

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

<В сторону>

Если в этот момент вы чувствуете растерянность, это потому, что код, скорее всего, предназначен для календарных вычислений, но на самом деле выполняет хронологические вычисления. Календарные вычисления - это вычисления с календарями.

Календари имеют всевозможные нарушения, например, месяцы и годы разной физической длины в днях. Календарное вычисление учитывает эти нарушения.

Хронологический расчет работает с фиксированными единицами и просто проверяет числа, не обращая внимания на календари. Хронологические вычисления не заботятся, используете ли вы григорианский календарь, юлианский календарь, индусский календарь, китайский календарь и т. Д.

</ В сторону>

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

Наконец, 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-битную арифметику. Это может быть достигнуто путем преобразования yearsв единицы измерения с repиспользованием более 32 бит в начале вычислений. Например:

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устройство никогда не создается в первую очередь. И мы даже не приблизиться к пределу при years, daysили year.

Хотя, если вы ожидали 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}лучше?
Болов
duration_castбыло бы хуже, потому что это плохая форма для использования duration_castдля неусечающих преобразований. Усеченные преобразования могут быть источником логических ошибок, и лучше использовать «большой молот» только тогда, когда вам это нужно, чтобы вы могли легко обнаружить усеченные преобразования в своем коде.
Говард Хиннант
1
Можно создать пользовательскую продолжительность:, use llyears = duration<long long, years::period>;а затем использовать ее вместо. Но, вероятно, лучше всего подумать о том, чего вы пытаетесь достичь, и спросить, правильно ли вы это делаете. Например, вам действительно нужна точность дня на шкале времени, которая составляет 10 тысяч лет? Гражданский календарь точен только до 1 дня за 4 тысячи лет. Возможно, тысячелетия с плавающей запятой были бы лучшей единицей?
Говард Хиннант
Пояснение: хронографическое моделирование гражданского календаря является точным в диапазоне от -32767/1/1 до 32767/12/31. Точность гражданского календаря в отношении моделирования солнечной системы составляет всего около 1 дня за 4 тысячи лет.
Говард Хиннант
1
Это действительно будет зависеть от варианта использования, и в настоящее время у меня возникают проблемы с выбором мотивирующего варианта использования для добавления yearsи days. Это буквально прибавляет кратное 365,2425 дня к целому числу дней. Обычно, если вы хотите сделать хронологический расчет порядка месяцев или лет, он должен моделировать физику или биологию. Возможно , этот пост по - разному , чтобы добавить monthsк system_clock::time_pointпомогло бы прояснить разницу между этими двумя типами вычислений: stackoverflow.com/a/43018120/576911
Говард Hinnant