Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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]

like image 996
sandthorn Avatar asked Mar 13 '20 10:03

sandthorn


People also ask

What is std :: Chrono :: duration?

Class template std::chrono::duration represents a time interval. It consists of a count of ticks of type Rep and a tick period, where the tick period is a compile-time rational fraction representing the time in seconds from one tick to the next. The only data stored in a duration is a tick count of type Rep .

What does std :: Chrono :: System_clock :: now return?

std::chrono::system_clock::now Returns a time point representing with the current point in time.


2 Answers

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

like image 200
Andrey Semashev Avatar answered Oct 16 '22 21:10

Andrey Semashev


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;
like image 39
Howard Hinnant Avatar answered Oct 16 '22 22:10

Howard Hinnant