Чи дійсно зберігання std :: chrono :: років принаймні 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 ]


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 з одиницями 216s.
Говард Хіннант

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самому справі. => operator int Я думаю, що це підтримує at least 17 bits yearsреалізацію.
пісочниця

Чи не проти відредагувати свою відповідь? Виявляється, std :: chrono :: років насправді int та std :: chrono :: рік максимум на 32767 довільно ..
sandthorn

@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}- це durationa, що periodдорівнює ratio<31'556'952>і значення, рівне 0. Зауважимо, що years{1}, виражене як з плаваючою комою days, рівно 365,2425. Це середнє значення тривалість цивільного року.

Підвираз days{365}- це durationa, що periodдорівнює ratio<86'400>і значення, рівне365 .

Підвираз years{0} + days{365}- це durationa, що periodдорівнює ratio<216>і значення, рівне 146'000. Це формується шляхом знаходження першого common_type_tз ratio<31'556'952>іratio<86'400> який є НОД (31'556'952, 86'400), або 216. Бібліотека спочатку перетворює обидва операнда до цієї загальної одиниці, а потім роблять додавання в загальному блоці.

Для перетворення years{0} в одиниці з періодом 216s потрібно помножити 0 на 146'097. Це буває дуже важливим моментом. Це перетворення може легко викликати переповнення, якщо виконати лише 32 біти.

<біля>

Якщо в цей момент ви відчуваєте плутанину, це тому, що код, ймовірно, передбачає календарі обчислення, але насправді робить хронологічні обчислення. Календарі обчислення - це обчислення з календарями.

У календарях існують всілякі порушення, такі як місяці та роки різної фізичної тривалості по днях. Календарі обчислення враховують ці порушення.

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

</aside>

Далі ми беремо нашу 146000[216]sтривалість і перетворити його в період з periodз ratio<86'400>(який має тип-псевдонім з ім'ям days). Ця функція floor<days>()робить це перетворення, і результат 365[86400]s, або простіше кажучи, справедливий 365d.

Наступним кроком є durationта перетворює його в a 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примусовий до 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;

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.