Apakah penyimpanan std :: chrono :: tahun benar-benar setidaknya 17 bit?

14

Dari cppreference

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

Menggunakan libc++, tampaknya penyimpanan garis bawah std::chrono::yearsadalah shortyang ditandatangani 16 bit .

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

Apakah ada kesalahan ketik pada cppreference atau yang lainnya?

Contoh:

#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);
}

[ Tautan Godbolt ]

sandthorn
sumber
2
yearrange: eel.is/c++draft/time.cal.year#members-19 years range: eel.is/c++draft/time.syn . yearadalah "nama" tahun sipil dan membutuhkan 16 bit. yearsadalah durasi chrono, bukan hal yang sama dengan a year. Satu dapat mengurangi dua yeardan hasilnya memiliki tipe years. yearsdiperlukan untuk dapat memegang hasil year::max() - year::min().
Howard Hinnant
1
std::chrono::years( 30797 ) + 365dtidak dikompilasi.
Howard Hinnant
1
Hasilnya years{30797} + days{365}adalah 204528013 dengan unit 216-an.
Howard Hinnant
1
Itu baru dua durasi yang ditambahkan. Melarang itu berarti melarang hours{2} + seconds{5}.
Howard Hinnant
4
Dugaan saya adalah bahwa Anda mengacaukan komponen kalender dengan jenis durasi karena mereka memang memiliki nama yang mirip. Berikut aturan umum: durationnama adalah jamak: years, months, days. Nama komponen penanggalan adalah tunggal: year, month, day. year{30797} + day{365}adalah kesalahan waktu kompilasi. year{2020}tahun ini. years{2020}berdurasi 2020 tahun.
Howard Hinnant

Jawaban:

8

Artikel cppreference benar . Jika libc ++ menggunakan tipe yang lebih kecil maka ini tampaknya menjadi bug di libc ++.

Andrey Semashev
sumber
Tetapi menambahkan yang lain wordyang mungkin jarang digunakan tidak akan menjadi bulking year_month_dayvektor yang tidak perlu? Mungkinkah itu at least 17 bitstidak dihitung sebagai teks norminal?
sandthorn
3
@sandthorn year_month_daymengandung year, bukan years. Representasi yeartidak harus 16-bit, meskipun tipe shortdigunakan sebagai eksposisi. OTOH, bagian 17 bit dalam yearsdefinisi adalah normatif karena tidak ditandai sebagai eksposisi saja. Dan terus terang, mengatakan bahwa itu setidaknya 17 bit dan kemudian tidak memerlukannya tidak ada artinya.
Andrey Semashev
1
Ah yearin year_month_daytampaknya intmemang. => operator int Saya pikir ini mendukung at least 17 bits yearsimplementasi.
sandthorn
Maukah Anda mengedit jawaban Anda? Ternyata std :: chrono :: tahun sebenarnya int dan std :: chrono :: tahun adalah maks pada 32767 arbitarily ..
sandthorn
@andthorn Jawabannya benar, saya tidak melihat mengapa saya perlu mengeditnya.
Andrey Semashev
4

Saya merinci contoh di https://godbolt.org/z/SNivyp sepotong demi sepotong:

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

Penyederhanaan dan asumsi using namespace std::chronoada dalam ruang lingkup:

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

Sub-ekspresi years{0}adalah durationdengan dengan periodsama dengan ratio<31'556'952>dan nilai sama dengan 0. Perhatikan bahwa years{1}, dinyatakan sebagai floating-point days, tepat 365.2425. Ini adalah panjang rata - rata tahun sipil.

Sub-ekspresi days{365}adalah durationdengan dengan periodsama dengan ratio<86'400>dan nilai sama dengan 365.

Sub-ekspresi years{0} + days{365}adalah durationdengan dengan periodsama dengan ratio<216>dan nilai sama dengan 146'000. Hal ini dibentuk oleh temuan pertama common_type_tdari ratio<31'556'952>dan ratio<86'400>yang merupakan GCD (31'556'952, 86'400), atau 216. perpustakaan pertama bertobat kedua operan unit umum ini, dan kemudian melakukan penambahan di unit umum.

Untuk mengkonversi years{0}ke unit dengan periode 216s seseorang harus mengalikan 0 dengan 146'097. Ini kebetulan menjadi poin yang sangat penting. Konversi ini dapat dengan mudah menyebabkan overflow bila dilakukan dengan hanya 32 bit.

<samping>

Jika pada saat ini Anda merasa bingung, itu karena kode tersebut kemungkinan bermaksud perhitungan kalender , tetapi sebenarnya melakukan perhitungan kronologis . Perhitungan kalender adalah perhitungan dengan kalender.

Kalender memiliki segala macam penyimpangan, seperti bulan dan tahun yang memiliki panjang fisik yang berbeda dalam hal hari. Perhitungan kalender memperhitungkan penyimpangan ini.

Perhitungan kronologis bekerja dengan unit-unit tetap, dan hanya menghitung angka tanpa memperhatikan kalender. Perhitungan kronologis tidak peduli jika Anda menggunakan kalender Gregorian, kalender Julian, kalender Hindu, kalender Cina, dll.

</aside>

Berikutnya kita mengambil kami 146000[216]sdurasi dan mengubahnya menjadi durasi dengan perioddari ratio<86'400>(yang memiliki jenis-alias bernama days). Fungsi floor<days>()melakukan konversi ini dan hasilnya adalah 365[86400]s, atau lebih sederhana, adil 365d.

Langkah selanjutnya mengambil durationdan mengubahnya menjadi a time_point. Jenis time_pointis time_point<system_clock, days>yang memiliki nama alias sys_days. Ini hanyalah hitungan dayssejak system_clockzaman, yaitu 1970-01-01 00:00:00 UTC, tidak termasuk detik kabisat.

Akhirnya sys_daysdikonversi menjadi year_month_daydengan nilai 1971-01-01.

Cara sederhana untuk melakukan perhitungan ini adalah:

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

Pertimbangkan perhitungan serupa ini:

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

Ini menghasilkan tanggal 16668-12-31. Yang mungkin sehari lebih awal dari yang Anda harapkan ((14699 + 1970) -01-01). Subexpression yang years{14699} + days{0}sekarang: 2'147'479'803[216]s. Perhatikan bahwa nilai run-time mendekati INT_MAX( 2'147'483'647), dan bahwa yang mendasari repkeduanya yearsdan daysadalah int.

Memang jika Anda mengkonversi years{14700}ke unit [216]sAnda mendapatkan overflow: -2'147'341'396[216]s.

Untuk mengatasinya, alihkan ke perhitungan kalender:

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

Semua hasil di https://godbolt.org/z/SNivyp yang menambahkan yearsdan daysdan menggunakan nilai untuk yearsyang lebih besar dari 14.699 mengalami intmeluap.

Jika seseorang benar-benar ingin melakukan perhitungan kronologis dengan yearsdan dengan dayscara ini, maka akan lebih bijaksana untuk menggunakan aritmatika 64 bit. Ini dapat dicapai dengan mengkonversi yearske unit dengan repmenggunakan lebih besar dari 32 bit di awal perhitungan. Sebagai contoh:

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

Dengan menambahkan 0ske years, ( secondsharus memiliki minimal 35 bit), maka common_type repterpaksa 64 bit untuk penambahan pertama ( years{14700} + 0s) dan berlanjut di 64 bit saat menambahkan days{0}:

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

Namun cara lain untuk menghindari luapan menengah (di kisaran ini) adalah untuk memotong yearsuntuk dayspresisi sebelum menambahkan lebih banyak days:

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

jmemiliki nilai 16669-12-31. Ini menghindari masalah karena sekarang [216]sunit tidak pernah dibuat di tempat pertama. Dan kita bahkan tidak pernah mendekati batas untuk years, daysatau year.

Meskipun jika Anda mengharapkan 16700-01-01, maka Anda masih memiliki masalah, dan cara untuk memperbaikinya adalah dengan melakukan perhitungan kalender sebagai gantinya:

year_month_day j = (1970y + years{14700})/1/1;
Howard Hinnant
sumber
1
Penjelasan yang bagus. Saya khawatir tentang perhitungan kronologisnya. Jika saya melihat years{14700} + 0s + days{0}dalam basis kode, saya tidak tahu apa 0syang dilakukan di sana dan seberapa penting hal itu. Apakah ada cara alternatif, mungkin lebih eksplisit? Apakah sesuatu seperti duration_cast<seconds>(years{14700}) + days{0}lebih baik?
bolov
duration_castakan lebih buruk karena itu bentuk yang buruk untuk digunakan duration_castuntuk konversi yang tidak terpotong. Memotong konversi dapat menjadi sumber kesalahan logika, dan yang terbaik adalah hanya menggunakan "palu besar" saat Anda membutuhkannya, sehingga Anda dapat dengan mudah menemukan konversi pemotongan dalam kode Anda.
Howard Hinnant
1
Orang dapat membuat durasi khusus:, use llyears = duration<long long, years::period>;dan kemudian menggunakannya. Tetapi mungkin yang terbaik adalah memikirkan apa yang ingin Anda capai dan mempertanyakan apakah Anda melakukannya dengan cara yang benar. Misalnya apakah Anda benar-benar membutuhkan ketepatan hari dalam skala waktu 10 ribu tahun? Kalender sipil hanya akurat sekitar 1 hari dalam 4 ribu tahun. Mungkin milenium floating point akan menjadi unit yang lebih baik?
Howard Hinnant
Klarifikasi: pemodelan kalender sipil chrono tepat di kisaran -32767/1/1 hingga 32767/12/31. Akurasi kalender sipil sehubungan dengan pemodelan tata surya hanya sekitar 1 hari dalam 4 ribu tahun.
Howard Hinnant
1
Ini akan sangat tergantung pada use case dan saya saat ini mengalami kesulitan memikirkan use case yang memotivasi untuk ditambahkan yearsdan days. Ini secara harfiah menambahkan beberapa kelipatan dari 365,2425 hari ke beberapa hari yang tidak terpisahkan. Biasanya jika Anda ingin melakukan perhitungan kronologis pada urutan bulan atau tahun, itu adalah model beberapa fisika atau biologi. Mungkin posting ini tentang cara-cara yang berbeda untuk menambahkan monthske system_clock::time_pointakan membantu memperjelas perbedaan antara dua jenis perhitungan: stackoverflow.com/a/43018120/576911
Howard Hinnant