[PATCH] libstdc++: hashing support for chrono value classes (P2592R2)
Tomasz Kaminski
tkaminsk@redhat.com
Thu Apr 24 13:30:32 GMT 2025
Hi,
I am reattaching the original patch below, as I wasn't on the mailing list
when it was sent.
Thank you for submitting the patch and apologies for the late response.
The major comment I have is that these are new C++26 classes, so we can use
requires __is_hash_enabled_for<_Tp> and define only enabled specialization.
In other cases we will be using a primary template that is disabled.
Also, argument_type and result_type typedefs are no longer present since
C++20, so you can remove them, and also remove __hash_base base classes
that were responsible for providing them.
To simplify searching, I have added `## COMMENT` before comments.
-----
> This commit implements [time.hash], added by P2592 for C++26.
> The implementation of the various hash specializations is
> mostly straightforward:
>
> * duration hashes its representation (not the period);
> * time_point hashes its duration;
> * the calendaring classes (year, month, day, etc.) hash their
> values;
> * zoned_time hashes the time zone pointer and its time point.
>
> There are however a couple of challenges:
>
> * The noexcept specifications for hashing duration, time_point,
> zoned_time are slightly more convoluted than expected, as
> their getters are noexcept(false) (e.g. calling count() on a
> duration will copy the representation and that may throw);
>
> * [time.duration] says that "Rep shall be an arithmetic type or a
> class emulating an arithmetic type". Technically speaking, this
> means that `const int` is a valid Rep; but we can't use
> hash<const int>.
>
> I'm not sure if this is deliberate or not (cf. LWG951, LWG953),
> but I've decided to support it nonetheless.
## COMMENT
We support them for optional, so this is consistent.
> * zoned_time and the calendar classes that combine several
> parts (e.g. month_day) need a hash combiner. The one
> available in _Hash_impl works on objects representations,
> not values, and given the nature of the calendar classes
> I'm very afraid that I may accidentally be hashing padding
> bits. Therefore, I've added a helper convenience combiner.
>
> libstdc++-v3/ChangeLog:
>
> * include/bits/functional_hash.h: Add a convenience hash
> combiner.
## COMMENT
The format here is to mention modified entity name in parenthesis:
include/bits/functional_hash.h (_Hash_combiner): Define.
https://www.gnu.org/prep/standards/html_node/Style-of-Change-Logs.html#Style-of-Change-Logs
> * include/bits/version.def: Bump the feature-testing macro.
> * include/bits/version.h: Regenerate.
> * include/std/chrono: Add std::hash specializations for the
> value classes in namespace chrono.
> * testsuite/std/time/hash.cc: New test.
>
> Signed-off-by: Giuseppe D'Angelo <giuseppe.dangelo@kdab.com>
> ---
> libstdc++-v3/include/bits/functional_hash.h | 31 ++
> libstdc++-v3/include/bits/version.def | 6 +
> libstdc++-v3/include/bits/version.h | 7 +-
> libstdc++-v3/include/std/chrono | 299 ++++++++++++++++++++
> libstdc++-v3/testsuite/std/time/hash.cc | 225 +++++++++++++++
> 5 files changed, 567 insertions(+), 1 deletion(-)
> create mode 100644 libstdc++-v3/testsuite/std/time/hash.cc
>
> diff --git a/libstdc++-v3/include/bits/functional_hash.h
b/libstdc++-v3/include/bits/functional_hash.h
> index 3626ebe816b..c0f82601b4b 100644
> --- a/libstdc++-v3/include/bits/functional_hash.h
> +++ b/libstdc++-v3/include/bits/functional_hash.h
> @@ -235,6 +235,37 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> { return hash(&__val, sizeof(__val), __hash); }
> };
>
> +#if __cplusplus >= 201103L // C++11
> + // A convenience hash combiner
> + struct _Hash_combiner
> + {
> + static void __hash_combine(size_t&) {}
> +
> + template<typename _Arg, typename... _Args>
> + static void
> + __hash_combine(size_t& __result,
> + const _Arg& __arg,
> + const _Args&... __args)
> + {
> + const size_t __arg_hash = hash<_Arg>{}(__arg);
> + __result = _Hash_impl::__hash_combine(__arg_hash, __result);
> + __hash_combine(__result, __args...);
> + }
> +
> + static size_t __hash()
> + { return 0; }
## COMMENT
I hope we never call this overload, so I would remove it.
+
> + template<typename _Arg, typename... _Args>
> + static size_t __hash(const _Arg& __arg, const _Args&... __args)
> + {
> + const size_t __arg_hash = hash<_Arg>{}(__arg);
> + size_t __result = _Hash_impl::hash(__arg_hash);
## COMMENT
I think you can implement this using fold expression, as:
(_Hash_impl::__hash_combine(hash<_Arg>{}(__args), __result), ...);
> + __hash_combine(__result, __args...);
> + return __result;
> + }
> + };
> +#endif // C++11
> +
> /// Specialization for float.
> template<>
> struct hash<float> : public __hash_base<size_t, float>
> diff --git a/libstdc++-v3/include/bits/version.def
b/libstdc++-v3/include/bits/version.def
> index 59b028c44af..2a2bbf54b3f 100644
> --- a/libstdc++-v3/include/bits/version.def
> +++ b/libstdc++-v3/include/bits/version.def
> @@ -575,6 +575,12 @@ ftms = {
>
> ftms = {
> name = chrono;
> + values = {
> + v = 202306;
> + cxxmin = 26;
> + hosted = yes;
> + cxx11abi = yes;
> + };
> values = {
> v = 201907;
> cxxmin = 20;
> diff --git a/libstdc++-v3/include/bits/version.h
b/libstdc++-v3/include/bits/version.h
> index e465131d99b..c75cb009ada 100644
> --- a/libstdc++-v3/include/bits/version.h
> +++ b/libstdc++-v3/include/bits/version.h
> @@ -644,7 +644,12 @@
> #undef __glibcxx_want_boyer_moore_searcher
>
> #if !defined(__cpp_lib_chrono)
> -# if (__cplusplus >= 202002L) && _GLIBCXX_USE_CXX11_ABI &&
_GLIBCXX_HOSTED
> +# if (__cplusplus > 202302L) && _GLIBCXX_USE_CXX11_ABI &&
_GLIBCXX_HOSTED
> +# define __glibcxx_chrono 202306L
> +# if defined(__glibcxx_want_all) || defined(__glibcxx_want_chrono)
> +# define __cpp_lib_chrono 202306L
> +# endif
> +# elif (__cplusplus >= 202002L) && _GLIBCXX_USE_CXX11_ABI &&
_GLIBCXX_HOSTED
> # define __glibcxx_chrono 201907L
> # if defined(__glibcxx_want_all) || defined(__glibcxx_want_chrono)
> # define __cpp_lib_chrono 201907L
> diff --git a/libstdc++-v3/include/std/chrono
b/libstdc++-v3/include/std/chrono
> index 7ffa5360728..8c6b173bc5d 100644
> --- a/libstdc++-v3/include/std/chrono
> +++ b/libstdc++-v3/include/std/chrono
> @@ -50,6 +50,10 @@
> # include <bits/unique_ptr.h>
> #endif
>
> +#if __cplusplus > 202302L
> +# include <bits/functional_hash.h>
> +#endif
> +
> #define __glibcxx_want_chrono
> #define __glibcxx_want_chrono_udls
> #include <bits/version.h>
> @@ -3329,6 +3333,301 @@ namespace __detail
> #endif // C++20
> } // namespace chrono
>
> +#if __glibcxx_chrono >= 202306 // C++26
> + // Hash support [time.hash]
> +
> + // duration
> + template<typename _Rep, typename _Period,
> + typename _ActualRep = remove_cv_t<_Rep>,
> + bool = __poison_hash<_ActualRep>::__enable_hash_call>
> + struct __chrono_duration_hash_base
> + {
> + size_t
> + operator()(const chrono::duration<_Rep, _Period>& __val) const
> + noexcept(
> + is_nothrow_copy_constructible_v<_Rep> &&
> + noexcept(hash<_ActualRep>{}(std::declval<_Rep>()))
> + )
> + {
> + return hash<_ActualRep>{}(__val.count());
> + }
> + };
> +
> + template<typename _Rep, typename _Period, typename _ActualRep>
> + struct __chrono_duration_hash_base<_Rep, _Period, _ActualRep, false>
> + {};
> +
> + template<typename _Rep, typename _Period>
> + struct hash<chrono::duration<_Rep, _Period>>
> + : private __poison_hash<remove_cv_t<_Rep>>,
> + public __chrono_duration_hash_base<_Rep, _Period>
## COMMENT
I would just replace that with requires __is_hash_enabled_for<_Rep>. For
not formattable durations we will end up in a base template that is
disabled. And place the operator() directly in the class.
> + {
> + using result_type [[__deprecated__]] = size_t;
> + using argument_type [[__deprecated__]] = chrono::duration<_Rep,
_Period>;
## COMMENT
These typedefs are only defined for __cplusplus < 202002L, and this are for
C++26 or later,
so we can simplu remove them.
> + };
> +
> + template<typename _Rep, typename _Period>
> + struct __is_fast_hash<hash<chrono::duration<_Rep, _Period>>>
> + : __is_fast_hash<hash<_Rep>>
> + {};
> +
> + // time_point
> + template<typename _Clock, typename _Dur,
> + bool = __poison_hash<_Dur>::__enable_hash_call>
> + struct __chrono_time_point_hash_base
> + {
> + size_t
> + operator()(const chrono::time_point<_Clock, _Dur>& __val) const
> + noexcept(
> + is_nothrow_copy_constructible_v<_Dur> &&
> + noexcept(hash<_Dur>{}(std::declval<_Dur>()))
> + )
> + {
> + return hash<_Dur>{}(__val.time_since_epoch());
> + }
> + };
> +
> + template<typename _Clock, typename _Dur>
> + struct __chrono_time_point_hash_base<_Clock, _Dur, false>
> + {};
> +
> + template<typename _Clock, typename _Dur>
> + struct hash<chrono::time_point<_Clock, _Dur>>
> + : private __poison_hash<_Dur>,
> + public __chrono_time_point_hash_base<_Clock, _Dur>
## COMMENT
I would just replace that with requires __is_hash_enabled_for<Dur>. For not
formattable durations we will end up in a base template that is disabled.
And place the operator() directly.
>
> + {
> + using result_type [[__deprecated__]] = size_t;
> + using argument_type [[__deprecated__]] =
chrono::time_point<_Clock, _Dur>;
> + };
> +
> + template<typename _Clock, typename _Dur>
> + struct __is_fast_hash<hash<chrono::time_point<_Clock, _Dur>>>
> + : __is_fast_hash<hash<_Dur>>
> + {};
> +
> + // day
> + template<>
> + struct hash<chrono::day>
> + : public __hash_base<size_t, chrono::day>
## COMMENT
We also do not need__hash_base, that only reason for existence is to
provide result_type and argument_type members, that we define only before
C++20.
> + {
> + size_t operator()(chrono::day __val) const noexcept
> + { return static_cast<unsigned>(__val); }
> + };
> +
> + // month
> + template<>
> + struct hash<chrono::month>
> + : public __hash_base<size_t, chrono::month>
> + {
> + size_t operator()(chrono::month __val) const noexcept
> + { return static_cast<unsigned>(__val); }
> + };
> +
> + // year
> + template<>
> + struct hash<chrono::year>
> + : public __hash_base<size_t, chrono::year>
> + {
> + size_t operator()(chrono::year __val) const noexcept
> + {
> + const int __tmp = static_cast<int>(__val);
> + return static_cast<size_t>(__tmp);
> + }
> + };
> +
> + // weekday
> + template<>
> + struct hash<chrono::weekday>
> + : public __hash_base<size_t, chrono::weekday>
> + {
> + size_t operator()(chrono::weekday __val) const noexcept
> + { return static_cast<size_t>(__val.c_encoding()); }
> + };
> +
> + // weekday_indexed
> + template<>
> + struct hash<chrono::weekday_indexed>
> + : public __hash_base<size_t, chrono::weekday_indexed>
> + {
> + size_t operator()(chrono::weekday_indexed __val) const noexcept
> + {
> + return _Hash_combiner::__hash(__val.weekday(), __val.index());
> + }
> + };
> +
> + // weekday_last
> + template<>
> + struct hash<chrono::weekday_last>
> + : public __hash_base<size_t, chrono::weekday_last>
> + {
> + size_t operator()(chrono::weekday_last __val) const noexcept
> + { return static_cast<size_t>(__val.weekday().c_encoding()); }
> + };
> +
> + // month_day
> + template<>
> + struct hash<chrono::month_day>
> + : public __hash_base<size_t, chrono::month_day>
> + {
> + size_t operator()(chrono::month_day __val) const noexcept
> + {
> + return _Hash_combiner::__hash(__val.month(), __val.day());
> + }
> + };
> +
> + // month_day_last
> + template<>
> + struct hash<chrono::month_day_last>
> + : public __hash_base<size_t, chrono::month_day_last>
> + {
> + size_t operator()(chrono::month_day_last __val) const noexcept
> + { return static_cast<unsigned>(__val.month()); }
> + };
> +
> + // month_weekday
> + template<>
> + struct hash<chrono::month_weekday>
> + : public __hash_base<size_t, chrono::month_weekday>
> + {
> + size_t operator()(chrono::month_weekday __val) const noexcept
> + {
> + return _Hash_combiner::__hash(__val.month(), __val.weekday_indexed());
> + }
> + };
> +
> + // month_weekday_last
> + template<>
> + struct hash<chrono::month_weekday_last>
> + : public __hash_base<size_t, chrono::month_weekday_last>
> + {
> + size_t
> + operator()(chrono::month_weekday_last __val) const noexcept
> + {
> + return _Hash_combiner::__hash(__val.month(), __val.weekday_last());
> + }
> + };
> +
> + // year_month
> + template<>
> + struct hash<chrono::year_month>
> + : public __hash_base<size_t, chrono::year_month>
> + {
> + size_t operator()(chrono::year_month __val) const noexcept
> + {
> + return _Hash_combiner::__hash(__val.year(), __val.month());
> + }
> + };
> +
> + // year_month_day
> + template<>
> + struct hash<chrono::year_month_day>
> + : public __hash_base<size_t, chrono::year_month_day>
> + {
> + size_t operator()(chrono::year_month_day __val) const noexcept
> + {
> + return _Hash_combiner::__hash(__val.year(),
> + __val.month(),
> + __val.day());
> + }
> + };
> +
> + // year_month_day_last
> + template<>
> + struct hash<chrono::year_month_day_last>
> + : public __hash_base<size_t, chrono::year_month_day_last>
> + {
> + size_t
> + operator()(chrono::year_month_day_last __val) const noexcept
> + {
> + return _Hash_combiner::__hash(__val.year(),
> + __val.month_day_last());
> + }
> + };
> +
> + // year_month_weekday
> + template<>
> + struct hash<chrono::year_month_weekday>
> + : public __hash_base<size_t, chrono::year_month_weekday>
> + {
> + size_t operator()(chrono::year_month_weekday __val) const noexcept
> + {
> + return _Hash_combiner::__hash(__val.year(),
> + __val.month(),
> + __val.weekday_indexed());
> + }
> + };
> +
> + // year_month_weekday_last
> + template<>
> + struct hash<chrono::year_month_weekday_last>
> + : public __hash_base<size_t, chrono::year_month_weekday_last>
> + {
> + size_t
> + operator()(chrono::year_month_weekday_last __val) const noexcept
> + {
> + return _Hash_combiner::__hash(__val.year(),
> + __val.month(),
> + __val.weekday_last());
> + }
> + };
> +
> + // zoned_time
> + template<typename _Duration, typename _TimeZonePtr,
> + typename _ActualDuration =
> + typename chrono::zoned_time<_Duration, _TimeZonePtr>::duration,
> + bool = (__poison_hash<_ActualDuration>::__enable_hash_call &&
> + __poison_hash<_TimeZonePtr>::__enable_hash_call)>
> + struct __chrono_zoned_time_hash_base
> + {
> + size_t
> + operator()
> + (const chrono::zoned_time<_Duration, _TimeZonePtr>& __val)
> + const
> + noexcept(
> + is_nothrow_copy_constructible_v<_ActualDuration> &&
> + noexcept(hash<_ActualDuration>{}(std::declval<_ActualDuration>())) &&
> + is_nothrow_copy_constructible_v<_TimeZonePtr> &&
> + noexcept(hash<_TimeZonePtr>{}(std::declval<_TimeZonePtr>()))
> + )
> + {
> + return _Hash_combiner::__hash(__val.get_sys_time(),
> + __val.get_time_zone());
> + }
> + };
> +
> + template<typename _Duration, typename _TimeZonePtr,
> + typename _ActualDuration>
> + struct __chrono_zoned_time_hash_base
> + <_Duration, _TimeZonePtr, _ActualDuration, false>
> + {};
> +
> + template<typename _Duration, typename _TimeZonePtr>
> + struct hash<chrono::zoned_time<_Duration, _TimeZonePtr>>
> + : private __poison_hash<_Duration>,
> + private __poison_hash<_TimeZonePtr>,
> + public __chrono_zoned_time_hash_base<_Duration, _TimeZonePtr>
## COMMENT
Same comment regarding using requires for __is_hash_enabled_for<_Duration>
&& __is_hash_enabled_for<_TimeZonePtr>.
> + {
> + using result_type [[__deprecated__]] = size_t;
> + using argument_type [[__deprecated__]] =
> + chrono::zoned_time<_Duration, _TimeZonePtr>;
## COMMENT
These are no longer present since C++20.
> + };
> +
> + template<typename _Duration, typename _TimeZonePtr>
> + struct __is_fast_hash<hash<chrono::zoned_time<_Duration,
_TimeZonePtr>>>
> + : bool_constant<__is_fast_hash<hash<_Duration>>::value &&
> + __is_fast_hash<hash<_TimeZonePtr>>::value>
> + {};
> +
> + // leap_second
> + template<>
> + struct hash<chrono::leap_second>
> + : public __hash_base<size_t, chrono::leap_second>
>
> + test_hash(steady_clock::now());
> + test_hash(utc_clock::now());
> + test_hash(gps_clock::now());
> +
> + // day
> + test_hash(1d);
> + test_hash(0d);
> + test_hash(255d);
> + test_hash(1234d);
> + test_hash(day(UINT_MAX));
> +
> + // month
> + test_hash(January);
> + test_hash(September);
> + test_hash(month(0u));
> + test_hash(month(255u));
> + test_hash(month(1234u));
> + test_hash(month(UINT_MAX));
> +
> + // year
> + test_hash(2024y);
> + test_hash(0y);
> + test_hash(year::min());
> + test_hash(year::max());
> + test_hash(year(INT_MAX));
> + test_hash(year(INT_MIN));
> +
> + // weekday
> + test_hash(Monday);
> + test_hash(Thursday);
> + test_hash(weekday(255u));
> + test_hash(weekday(UINT_MAX));
> +
> + // weekday_indexed
> + test_hash(Monday[0u]);
> + test_hash(Monday[7u]);
> + test_hash(Monday[1234u]);
> + test_hash(weekday(1234u)[0u]);
> +
> + // weekday_last
> + test_hash(Monday[last]);
> + test_hash(Friday[last]);
> + test_hash(weekday(1234u)[last]);
> +
> + // month_day
> + test_hash(March / 3);
> + test_hash(March / 31);
> + test_hash(February / 31);
> + test_hash(February / 1234);
> + test_hash(month(1234u) / 1);
> +
> + // month_day_last
> + test_hash(March / last);
> + test_hash(month(1234u) / last);
> +
> + // month_weekday
> + test_hash(March / Tuesday[2u]);
> + test_hash(month(1234u) / Tuesday[2u]);
> + test_hash(March / weekday(1234u)[2u]);
> + test_hash(March / Tuesday[1234u]);
> +
> + // month_weekday_last
> + test_hash(April / Sunday[last]);
> + test_hash(month(1234u) / Tuesday[last]);
> + test_hash(April / weekday(1234u)[last]);
> +
> + // year_month
> + test_hash(2024y / August);
> + test_hash(1'000'000y / August);
> + test_hash(2024y / month(1234u));
> +
> + // year_month_day
> + test_hash(2024y / August / 31);
> + test_hash(-10y / March / 5);
> + test_hash(2024y / February / 31);
> + test_hash(1'000'000y / March / 5);
> + test_hash(2024y / month(1234u) / 5);
> + test_hash(2024y / March / 1234);
> +
> + // year_month_day_last
> + test_hash(2024y / August / last);
> + test_hash(1'000'000y / August / last);
> + test_hash(2024y / month(1234u) / last);
> +
> + // year_month_weekday
> + test_hash(2024y / August / Tuesday[2u]);
> + test_hash(-10y / August / Tuesday[2u]);
> + test_hash(1'000'000y / August / Tuesday[2u]);
> + test_hash(2024y / month(1234u) / Tuesday[2u]);
> + test_hash(2024y / August / weekday(1234u)[2u]);
> + test_hash(2024y / August / Tuesday[1234u]);
> +
> + // year_month_weekday_last
> + test_hash(2024y / August / Tuesday[last]);
> + test_hash(-10y / August / Tuesday[last]);
> + test_hash(1'000'000y / August / Tuesday[last]);
> + test_hash(2024y / month(1234u) / Tuesday[last]);
> + test_hash(2024y / August / weekday(1234u)[last]);
> +
> + // zoned_time
> + test_hash(zoned_seconds("Europe/Rome", sys_seconds(1234s)));
> + test_hash(zoned_time("Europe/Rome", system_clock::now()));
> +
> + // leap_second
> + for (leap_second l : get_tzdb().leap_seconds)
> + test_hash(l);
> +}
> +
> +void test02()
> +{
> + using namespace std::chrono;
> + using namespace std::literals::chrono_literals;
> +
> + {
> + std::unordered_set<milliseconds> set;
> + set.insert(2000ms);
> + VERIFY(set.contains(2000ms));
> + VERIFY(set.contains(2s));
> + VERIFY(!set.contains(1234ms));
> + VERIFY(!set.contains(1234s));
> + }
> + {
> + using TP = sys_time<milliseconds>;
> + std::unordered_set<TP> set;
> + set.insert(TP(2000ms));
> + VERIFY(set.contains(TP(2000ms)));
> + VERIFY(set.contains(sys_seconds(2s)));
> + VERIFY(!set.contains(TP(1234ms)));
> + VERIFY(!set.contains(sys_seconds(1234s)));
> + }
> +}
> +
> +void test03()
> +{
> + using namespace std::chrono;
> +
> + const auto test = []<typename T>(const T& t)
> + {
> + static_assert(noexcept(std::hash<T>{}(t)));
> + };
> +
> + test(duration<const int>(123));
> + test(duration<const int, std::ratio<1, 1000>>(123));
> + test(duration<const int, std::ratio<1000, 1>>(123));
> + test(duration<const volatile double>(123.456));
> + test(duration<const
arithmetic_wrapper<int>>(arithmetic_wrapper<int>(123)));
> +}
## COMMENT
I would add a negative test here, to check if hash specializations are not
enabled if _Rep or _TimeZonePtr is not hashable. As one of the
differentiation between enabled and disabled hash is
is_default_constructible_v, I would check static assert for
!is_default_constructible_v.
> +
> +int main()
> +{
> + test01();
> + test02();
> + test03();
> +}
--
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://gcc.gnu.org/pipermail/libstdc++/attachments/20250424/5e604a61/attachment-0001.htm>
More information about the Libstdc++
mailing list