Reputation: 2870
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);
}
Upvotes: 14
Views: 941
Reputation: 218700
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;
Upvotes: 4
Reputation: 10606
The cppreference article is correct. If libc++ uses a smaller type then this seems to be a bug in libc++.
Upvotes: 8