Is std::chrono::years storage really at least 17 bit?
Asked Answered
V

2

14

From cppreference

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

Using libc++, it seems the underlining storage of std::chrono::years is short which is signed 16 bits.

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

Is there a typo on cppreference or anything else?

Example:

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

Vilberg answered 13/3, 2020 at 10:4 Comment(10)
year range: eel.is/c++draft/time.cal.year#members-19 years range: eel.is/c++draft/time.syn . year is the "name" of the civil year and requires 16 bits. years is a chrono duration, not the same thing as a year. One can subtract two year and the result has type years. years is required to be able to hold the result of year::max() - year::min().Tertiary
std::chrono::years( 30797 ) + 365d doesn't compile.Tertiary
The result of years{30797} + days{365} is 204528013 with units of 216s.Tertiary
@HowardHinnant, different floor duration arithmatics stuns me so bad. If not used, can we prohibit that at compile time?Vilberg
Sorry, I don't understand the question.Tertiary
@HowardHinnant years{30797} + days{365} : Can we prohibit this at compile time?Vilberg
That's just two durations being added. To prohibit it would mean prohibiting hours{2} + seconds{5}.Tertiary
My guess is that you're confusing calendrical components with duration types because they do have such similar names. Here's a general rule: duration names are plural: years, months, days. Calendrical component names are singular: year, month, day. year{30797} + day{365} is a compile-time error. year{2020} is this year. years{2020} is a duration 2020 years long.Tertiary
@HowardHinnant Would you mind please explain the inconsistency there => SNivypVilberg
I've added an answer that explains things. If you have further questions, I'm happy to amend my answer to address them.Tertiary
B
8

The cppreference article is correct. If libc++ uses a smaller type then this seems to be a bug in libc++.

Berberine answered 13/3, 2020 at 10:13 Comment(5)
But adding another word that probably barely used wouldn't be bulking year_month_day vectors unneccessarily? Could that at least 17 bits be not counted as norminal text?Vilberg
@Vilberg year_month_day contains year, not years. The representation of year is not required to be 16-bit, although the type short is used as exposition. OTOH, the 17 bits part in the years definition is normative as it is not marked as exposition only. And frankly, saying that it ts at least 17 bits and then not requiring it is meaningless.Berberine
Ah year in year_month_day seems to be int indeed. => operator int I think this supports at least 17 bits years implementation.Vilberg
Would you mind edit your answer? It turns out std::chrono::years is actually int and std::chrono::year is max at 32767 arbitarily..Vilberg
@Vilberg The answer is correct, I don't see why I would need to edit it.Berberine
T
4

I'm breaking down the example at https://godbolt.org/z/SNivyp piece by piece:

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

Simplifying and assuming using namespace std::chrono is in scope:

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

The sub-expression years{0} is a duration with a period equal to ratio<31'556'952> and a value equal to 0. Note that years{1}, expressed as floating-point days, is exactly 365.2425. This is the average length of the civil year.

The sub-expression days{365} is a duration with a period equal to ratio<86'400> and a value equal to 365.

The sub-expression years{0} + days{365} is a duration with a period equal to ratio<216> and a value equal to 146'000. This is formed by first finding the common_type_t of ratio<31'556'952> and ratio<86'400> which is the GCD(31'556'952, 86'400), or 216. The library first converts both operands to this common unit, and then does the addition in the common unit.

To convert years{0} to units with a period of 216s one must multiply 0 by 146'097. This happens to be a very important point. This conversion can easily cause overflow when done with only 32 bits.

<aside>

If at this point you feel confused, it is because the code likely intends a calendrical computation, but is actually doing a chronological computation. Calendrical computations are computations with calendars.

Calendars have all sorts of irregularities, such as months and years being of different physical lengths in terms of days. A calendrical computation takes these irregularities into account.

A chronological computation works with fixed units, and just cranks out the numbers without regard to calendars. A chronological computation doesn't care if you use the Gregorian calendar, the Julian calendar, the Hindu calendar, the Chinese calendar, etc.

</aside>

Next we take our 146000[216]s duration and convert it to a duration with a period of ratio<86'400> (which has a type-alias named days). The function floor<days>() does this conversion and the result is 365[86400]s, or more simply, just 365d.

The next step takes the duration and converts it into a time_point. The type of the time_point is time_point<system_clock, days> which has a type-alias named sys_days. This is simply a count of days since the system_clock epoch, which is 1970-01-01 00:00:00 UTC, excluding leap seconds.

Finally the sys_days is converted to a year_month_day with the value 1971-01-01.

A simpler way to do this computation is:

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

Consider this similar computation:

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

This results in the date 16668-12-31. Which is probably a day earlier than you were expecting ((14699+1970)-01-01). The subexpression years{14699} + days{0} is now: 2'147'479'803[216]s. Note that the run-time value is nearing INT_MAX (2'147'483'647), and that the underlying rep of both years and days is int.

Indeed if you convert years{14700} to units of [216]s you get overflow: -2'147'341'396[216]s.

To fix this, switch to a calendrical computation:

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

All of the results at https://godbolt.org/z/SNivyp that are adding years and days and using a value for years that is greater than 14699 are experiencing int overflow.

If one really wants to do chronological computations with years and days this way, then it would be wise to use 64 bit arithmetic. This can be accomplished by converting years to units with a rep using greater than 32 bits early in the computation. For example:

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

By adding 0s to years, (seconds must have at least 35 bits), then the common_type rep is forced to 64 bits for the first addition (years{14700} + 0s) and continues in 64 bits when adding days{0}:

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

Yet another way to avoid intermediate overflow (at this range) is to truncate years to days precision before adding more days:

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

j has the value 16669-12-31. This avoids the problem because now the [216]s unit is never created in the first place. And we never even get close to the limit for years, days or year.

Though if you were expecting 16700-01-01, then you still have a problem, and the way to correct it is to do a calendrical computation instead:

year_month_day j = (1970y + years{14700})/1/1;
Tertiary answered 16/3, 2020 at 14:45 Comment(6)
Great explanation. I am worried about the chronological computation. If I see years{14700} + 0s + days{0} in a codebase, I would have no idea what 0s is doing there and how important it is. Is there an alternate, maybe more explicit way? Would something like duration_cast<seconds>(years{14700}) + days{0} be better?Naman
duration_cast would be worse because it is bad form to use duration_cast for non-truncating conversions. Truncating conversions can be a source of logic errors, and it is best to only use the "big hammer" when you need it, so that you can easily spot the truncating conversions in your code.Tertiary
One could create a custom duration: use llyears = duration<long long, years::period>;, and then use that instead. But probably the best thing is to think about what you're trying to accomplish and question whether you're going about it the right way. For example do you really need day-precision on a time scale that is 10 thousand years? The civil calendar is only accurate to about 1 day in 4 thousand years. Perhaps a floating point millennia would be a better unit?Tertiary
Clarification: chrono's modeling of the civil calendar is exact in the range -32767/1/1 to 32767/12/31. The civil calendar's accuracy with respect to modeling the solar system is only about 1 day in 4 thousand years.Tertiary
@HowardHinnant Between sys_days{ years{14700} + 0s + days{1000} } versus sys_days{floor<days>(years{14700}) + days{1000} } which one you prefer personally?Vilberg
It would really depend on the use case and I'm currently having trouble thinking of a motivating use case to add years and days. This is literally adding some multiple of 365.2425 days to some integral number of days. Normally if you want to do a chronological computation on the order of months or years, it is to model some physics or biology. Perhaps this post on the different ways to add months to system_clock::time_point would help clarify the difference between the two types of computations: https://mcmap.net/q/663479/-c-add-months-to-chrono-system_clock-time_pointTertiary

© 2022 - 2024 — McMap. All rights reserved.