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]

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
sandthorn
  • 2,770
  • 1
  • 15
  • 59
  • 2
    `year` range: http://eel.is/c++draft/time.cal.year#members-19 `years` range: http://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()`. – Howard Hinnant Mar 13 '20 at 13:01
  • 1
    `std::chrono::years( 30797 ) + 365d` doesn't compile. – Howard Hinnant Mar 13 '20 at 13:17
  • 1
    The result of `years{30797} + days{365}` is 204528013 with units of 216s. – Howard Hinnant Mar 13 '20 at 13:18
  • @HowardHinnant, different floor duration arithmatics stuns me so bad. If not used, can we prohibit that at compile time? – sandthorn Mar 14 '20 at 12:58
  • Sorry, I don't understand the question. – Howard Hinnant Mar 14 '20 at 13:25
  • @HowardHinnant `years{30797} + days{365}` : Can we prohibit this at compile time? – sandthorn Mar 14 '20 at 13:26
  • 1
    That's just two durations being added. To prohibit it would mean prohibiting `hours{2} + seconds{5}`. – Howard Hinnant Mar 14 '20 at 13:42
  • 4
    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. – Howard Hinnant Mar 14 '20 at 14:01
  • @HowardHinnant Would you mind please explain the inconsistency there => [SNivyp](https://godbolt.org/z/SNivyp) – sandthorn Mar 16 '20 at 05:48
  • I've added an answer that explains things. If you have further questions, I'm happy to amend my answer to address them. – Howard Hinnant Mar 16 '20 at 16:15

2 Answers2

8

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

Andrey Semashev
  • 10,046
  • 1
  • 17
  • 27
  • 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? – sandthorn Mar 13 '20 at 11:39
  • 3
    @sandthorn `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. – Andrey Semashev Mar 13 '20 at 11:50
  • 1
    Ah `year` in `year_month_day` seems to be `int` indeed. => [operator int](https://en.cppreference.com/w/cpp/chrono/year/operator_int) I think this supports `at least 17 bits` `years` implementation. – sandthorn Mar 13 '20 at 11:57
  • Would you mind edit your answer? It turns out *std::chrono::years* is actually int and *std::chrono::year* is max at 32767 arbitarily.. – sandthorn Mar 17 '20 at 13:53
  • @sandthorn The answer is correct, I don't see why I would need to edit it. – Andrey Semashev Mar 17 '20 at 15:46
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;
Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • 1
    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(years{14700}) + days{0}` be better? – bolov Mar 16 '20 at 17:02
  • `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. – Howard Hinnant Mar 16 '20 at 17:15
  • 1
    One could create a custom duration: `use llyears = duration;`, 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? – Howard Hinnant Mar 16 '20 at 17:20
  • 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. – Howard Hinnant Mar 16 '20 at 17:27
  • @HowardHinnant Between `sys_days{ years{14700} + 0s + days{1000} }` versus `sys_days{floor(years{14700}) + days{1000} }` which one you prefer personally? – sandthorn Mar 17 '20 at 13:58
  • 1
    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://stackoverflow.com/a/43018120/576911 – Howard Hinnant Mar 17 '20 at 14:27