Czy pamięć std :: chrono :: years naprawdę ma co najmniej 17 bitów?

14

Z preferencji

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

Używając libc++, wydaje się, że podkreśleniem std::chrono::yearsjest shortto, że podpisano 16 bitów .

std::chrono::years( 30797 )        // yields  32767/01/01
std::chrono::years( 30797 ) + 365d // yields -32768/01/01 apparently UB

Czy istnieje literówka na preferencjach lub coś innego?

Przykład:

#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 ]

sandthorn
źródło
2
yearzakres: eel.is/c++draft/time.cal.year#members-19 years zakres: eel.is/c++draft/time.syn . yearto „nazwa” roku cywilnego i wymaga 16 bitów. yearsjest czasem trwania chrono, a nie tym samym co year. Można odjąć dwa, yeara wynik ma typ years. yearsmusi być w stanie utrzymać wynik year::max() - year::min().
Howard Hinnant
1
std::chrono::years( 30797 ) + 365dnie kompiluje się.
Howard Hinnant
1
Wynikiem years{30797} + days{365}jest 204528013 z jednostkami 216s.
Howard Hinnant
1
To tylko dwa dodane czasy. Zakaz oznaczałby zakaz hours{2} + seconds{5}.
Howard Hinnant
4
Domyślam się, że jesteś mylące calendrical komponentów z typami trwania, ponieważ nie mają tak podobne nazwy. Oto ogólna zasada: durationimiona są w liczbie mnogiej: years, months, days. Calendrical nazwy elementów są w liczbie pojedynczej: year, month, day. year{30797} + day{365}jest błędem czasu kompilacji. year{2020}jest w tym roku. years{2020}trwa 2020 lat.
Howard Hinnant

Odpowiedzi:

8

Artykuł o preferencjach jest poprawny . Jeśli libc ++ używa mniejszego typu, wydaje się to być błędem w libc ++.

Andrey Semashev
źródło
Ale dodanie kolejnego, wordktóry prawdopodobnie rzadko używany nie byłby year_month_dayniepotrzebnym łączeniem wektorów? Czy at least 17 bitsnie można tego uznać za tekst normalny?
sandthorn
3
@ sandthorn year_month_dayzawiera year, nie years. Reprezentacja yearnie musi być 16-bitowa, chociaż typ shortjest używany jako ekspozycja. OTOH, 17-bitowa część yearsdefinicji jest normatywna, ponieważ nie jest oznaczona tylko jako ekspozycja. I szczerze mówiąc, twierdzenie, że ma co najmniej 17 bitów, a następnie nie wymaganie, jest bez znaczenia.
Andrey Semashev
1
Ah yearw year_month_daywydaje się być intrzeczywiście. => operator int Myślę, że obsługuje to at least 17 bits yearsimplementację.
sandthorn
Czy mógłbyś edytować swoją odpowiedź? Okazuje się, że std :: chrono :: years jest właściwie int, a std :: chrono :: year jest maksymalny w 32767 arbitarnie ..
sandthorn
@sandthorn Odpowiedź jest prawidłowa, nie rozumiem, dlaczego musiałbym ją edytować.
Andrey Semashev
4

Rozbijam przykład na https://godbolt.org/z/SNivyp kawałek po kawałku:

  auto a = std::chrono::year_month_day( 
    sys_days( 
      std::chrono::floor<days>(
        std::chrono::years(0) 
        + std::chrono::days( 365 )
      )
    )
  );

Uproszczenie i założenie using namespace std::chronoobejmuje:

year_month_day a = sys_days{floor<days>(years{0} + days{365})};

Sub ekspresja years{0}jest durationz periodrówna ratio<31'556'952>i wartość równą 0. Zauważ, że years{1}wyrażony jako zmiennoprzecinkowy days, wynosi dokładnie 365.2425. Jest to średnia długość roku cywilnego.

Sub ekspresja days{365}jest durationz periodrówna ratio<86'400>i wartość równą 365.

Sub ekspresja years{0} + days{365}jest durationz periodrówna ratio<216>i wartość równą 146'000. To jest utworzone przez pierwszy ustaleniem common_type_tz ratio<31'556'952>i ratio<86'400>który jest GCD (31'556'952, 86'400) lub 216. Biblioteka najpierw konwertuje z argumentów do tej wspólnej jednostki, a następnie wykonuje się dodatkowe do wspólnego urządzenia.

Aby przeliczyć years{0}na jednostki z okresem 216s, należy pomnożyć 0 przez 146'097. Jest to bardzo ważny punkt. Ta konwersja może łatwo spowodować przepełnienie, gdy zostanie wykonana tylko z 32 bitami.

<aside>

Jeśli w tym momencie czujesz się zdezorientowany, dzieje się tak dlatego, że kod prawdopodobnie planuje obliczenia kalendarza , ale w rzeczywistości wykonuje obliczenia chronologiczne . Obliczenia kalendarzowe to obliczenia z kalendarzami.

Kalendarze mają różnego rodzaju nieregularności, takie jak miesiące i lata o różnej długości fizycznej pod względem dni. Obliczenia kalendarzowe uwzględniają te nieprawidłowości.

Obliczenia chronologiczne działają z ustalonymi jednostkami i po prostu korygują liczby bez względu na kalendarze. Obliczenia chronologiczne nie dbają o to, czy używasz kalendarza gregoriańskiego, kalendarza juliańskiego, kalendarza hinduskiego, kalendarza chińskiego itp.

</aside>

Następny bierzemy nasz 146000[216]sczas i przekształcić ją w czasie trwania z periodod ratio<86'400>(który ma typu alias o nazwie days). Funkcja floor<days>()wykonuje tę konwersję, a wynik jest 365[86400]s, lub prościej, sprawiedliwy 365d.

Następnym krokiem jest durationi konwertuje go na time_point. Typ time_pointjest time_point<system_clock, days>który ma typu alias o nazwie sys_days. Jest to po prostu odliczanie daysod system_clockepoki 1970-01-01 00:00:00 UTC, z wyłączeniem sekund przestępnych.

Na koniec sys_dayskonwertowane jest year_month_dayna wartość o wartości 1971-01-01.

Prostszym sposobem wykonania tego obliczenia jest:

year_month_day a = sys_days{} + days{365};

Rozważ to podobne obliczenie:

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

To powoduje datę 16668-12-31. Który jest prawdopodobnie dzień wcześniej, niż się spodziewałeś ((14699 + 1970) -01-01). Podwyrażenie years{14699} + days{0}jest teraz: 2'147'479'803[216]s. Zauważ, że wartość czasu wykonania jest bliska INT_MAX( 2'147'483'647), i że podstawą repzarówno yearsi daysjest int.

Rzeczywiście, jeśli przekonwertować years{14700}do jednostek [216]sdostać overflow: -2'147'341'396[216]s.

Aby to naprawić, przejdź do obliczeń kalendarza:

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

Wszystkie wyniki na https://godbolt.org/z/SNivyp , które dodają yearsi daysużywają do tego wartości yearswiększej niż 14699, ulegają intprzepełnieniu.

Jeśli ktoś naprawdę chce zrobić z obliczeń chronologicznych yearsi daysten sposób, to byłoby mądre, aby użyć 64 bitowej arytmetyki. Można to osiągnąć przez konwersję yearsna jednostki przy repużyciu więcej niż 32 bitów na początku obliczeń. Na przykład:

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

Dodając 0sdo years, ( secondsmusi mieć co najmniej 35 bitów), następnie common_type repjest zmuszany do 64 bitów dla pierwszego dodawania ( years{14700} + 0s) i kontynuuje w 64 bitach podczas dodawania days{0}:

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

Jeszcze innym sposobem uniknięcia pośredniego przepełnienia (w tym zakresie) jest obcięcie yearsdo daysprecyzji przed dodaniem kolejnych days:

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

jma wartość 16669-12-31. Pozwala to uniknąć problemu, ponieważ teraz [216]sjednostka nigdy nie jest tworzona. I nigdy nawet zbliżyć się do granicy years, dayslub year.

Chociaż jeśli się spodziewałeś 16700-01-01, nadal masz problem, a sposobem na jego rozwiązanie jest wykonanie obliczeń kalendarza:

year_month_day j = (1970y + years{14700})/1/1;
Howard Hinnant
źródło
1
Świetne wyjaśnienie. Martwię się o obliczenia chronologiczne. Jeśli widzę years{14700} + 0s + days{0}w bazie kodu, nie miałbym pojęcia, co 0stam robi i jak to jest ważne. Czy istnieje alternatywny, może bardziej wyraźny sposób? Czy coś takiego duration_cast<seconds>(years{14700}) + days{0}byłoby lepsze?
bolov
duration_castbyłoby gorzej, ponieważ jest to zła forma do zastosowania duration_castw przypadku konwersji nieobcinających. Obcinanie konwersji może być źródłem błędów logicznych i najlepiej używać „dużego młota” tylko wtedy, gdy jest to potrzebne, aby można było łatwo wykryć obcięte konwersje w kodzie.
Howard Hinnant
1
Można utworzyć niestandardowy czas trwania:, use llyears = duration<long long, years::period>;a następnie użyć go zamiast tego. Ale prawdopodobnie najlepszą rzeczą jest zastanowienie się nad tym, co próbujesz osiągnąć, i zastanowienie się, czy idziesz do tego we właściwy sposób. Na przykład, czy naprawdę potrzebujesz precyzji dnia w skali czasowej wynoszącej 10 tysięcy lat? Kalendarz cywilny jest dokładny z dokładnością do około 1 dnia na 4 tysiące lat. Może tysiąclecie zmiennoprzecinkowe byłoby lepszą jednostką?
Howard Hinnant
Wyjaśnienie: modelowanie kalendarza cywilnego przez chrono jest dokładne w zakresie od -32767/1/1 do 32767/12/31. Dokładność kalendarza cywilnego w odniesieniu do modelowania układu słonecznego wynosi tylko około 1 dzień na 4 tysiące lat.
Howard Hinnant
1
To naprawdę zależy od przypadku użycia i obecnie mam problem z wymyśleniem motywującego przypadku użycia do dodania yearsi days. To dosłownie dodaje pewną wielokrotność 365.2425 dni do pewnej integralnej liczby dni. Zwykle, jeśli chcesz wykonać obliczenia chronologiczne na podstawie miesięcy lub lat, jest to modelowanie fizyki lub biologii. Być może ten post na temat różnych sposobów dodawania monthsdo system_clock::time_pointpomógłby wyjaśnić różnicę między dwoma rodzajami obliczeń: stackoverflow.com/a/43018120/576911
Howard Hinnant