| Document #: | P3816R4 [Latest] [Status] |
| Date: | 2026-10-22 |
| Project: | Programming Language C++ |
| Audience: |
SG7 Reflection |
| Reply-to: |
Matt Cummins <mcummins16@bloomberg.net> Valentyn Yukhymenko <vyuhimenko@bloomberg.net> |
consteval_hashThis paper proposes a new standard library template, consteval_hash<T>,
with a single specialization for
meta::info.
The purpose of this facility is to provide a standard interface for
compile-time hashing, thereby allowing unordered containers such as
unordered_map and
unordered_set to be used with
meta::info
keys, and potentially with other types in the future.
consteval auto compile_time_function() -> void
{
const auto hasher = consteval_hash<meta::info>{}; // proposed
const size_t h = hasher(^^::);
// now possible
unordered_map<meta::info, int, consteval_hash<meta::info>> m;
unordered_set<meta::info, consteval_hash<meta::info>> s;
}Any hashing feature in the direction of P3816 should require cross-TU hash stability (with caveats for version drift).
| SF | F | N | A | SA |
| 9 | 16 | 0 | 0 | 0 |
Result: consensus
EWG encourage the authors to explore the possibility of fully specifying the hash result.
| SF | F | N | A | SA |
| 1 | 2 | 9 | 4 | 2 |
Result: not consensus
EWG encourages more work in the direction of P3816R3.
| SF | F | N | A | SA |
| 5 | 18 | 1 | 0 | 0 |
Result: consensus
Encourage more work in the direction of P3816R2 (Hashing meta::info), and see it again after incorporating the feedback received/better exploring the stability guarantees.
| SF | F | N | A | SA |
| 7 | 22 | 1 | 0 | 0 |
Result: consensus
[P2996] introduces
meta::info
to represent reflections of C++ constructs. Hashing support was
intentionally omitted, as it lies outside the core reflection
feature.
[P3372] makes unordered associative
containers
constexpr.
This creates a need for compile-time hashing, including support for
meta::info
and other consteval-only types as keys.
A robust hash for
meta::info
requires compiler support; we therefore believe such a facility belongs
in the standard library.
Given that this proposal focuses on enabling unordered associative
containers keyed on
meta::info,
it is natural to also ask what meaning, if any, should be assigned to
map<meta::info, T>
and set<meta::info>.
Both would require less<meta::info>,
and hence operator<,
to be well-defined. However, reflections do not admit a natural or
semantically meaningful total ordering. In addition, [P2830] (4.1.4) states that any operator<=>
defined for std::meta::info
should be consistent with the compile-time type ordering it
proposes.
Ultimately, defining such an ordering introduces additional semantic challenges and design complexity, so this proposal intentionally restricts its scope to hashing and leaves ordering of reflections for future work. Readers interested in this should see [P4032].
Enabling hash-based associative containers as a first step will improve the ergonomics of compile-time programming and bring it more in line with runtime code.
In terms of design, a straightforward approach would be to specialize
hash<meta::info>,
but hash<T>
in general is not constexpr-friendly due to its runtime requirements, so
it would be inconsistent with compile-time hashing of other types. As
such, this paper proposes a dedicated facility for compile-time hashing
which can be extended to other types in the future.
The following examples illustrate practical applications of
consteval_hash.
mp_unique using reflectionIn [P2830] (7.3) the authors discuss the
infeasibility of implementing
mp_unique using value-based
reflection. Our proposal provides a short and effective solution without
needing to sort a list of reflected types.
template <typename... Types>
struct type_list {};
template <typename TypeList>
consteval auto mp_unique_reflected()
{
static_assert(meta::has_template_arguments(^^TypeList), "mp_unique requires a type_list");
static_assert(meta::template_of(^^TypeList) == ^^type_list, "mp_unique requires a type_list");
unordered_set<meta::info, consteval_hash<meta::info>> seen;
vector<meta::info> unique_types;
for (auto type_info : meta::template_arguments_of(^^TypeList)) {
if (const bool is_unique = seen.insert(type_info).second; is_unique) {
unique_types.push_back(type_info);
}
}
return meta::substitute(^^type_list, unique_types);
}
template <class TypeList>
using mp_unique = [:mp_unique_reflected<TypeList>():];
using input = type_list<int, char, int, string, double, char>;
using filtered = mp_unique<input>;
using expected = type_list<int, char, string, double>;
static_assert(is_same_v<expected, filtered>);name_of functionIn [P2996] (4.4.6), the authors discuss the
challenges of producing user-friendly names for reflected entities and
argue that functions such as name_of
should be implemented by third-party C++ libraries rather than
standardized, with the standard providing the necessary lower-level
tools to do so. To implement such a function, one might define a custom
mapping of known types or entities to more descriptive labels.
Using unordered_map for the
mapping provides a terser implementation:
Without map
|
With map
|
|---|---|
|
|
[P3394] introduces annotations for reflection and provides two query functions:
consteval vector<info> annotations_of(info item);
consteval vector<info> annotations_of_with_type(info item, info type);The second function is provided because grouping annotations by their type is a common operation. If a standard compile-time hashing facility was available, then for cases that require calling this function several times with different types, it might be more ergonomic to have an additional function:
consteval unordered_map<info, vector<info>, consteval_hash<info>> annotations_of_by_type(info item);This proposal is a pure library addition and does not depend on any
other library extensions. It introduces the first example of a
compile-time hash facility. Moreover, it enables future standard library
features: new
consteval
functions may naturally require unordered containers as part of their
interfaces, and consteval_hash
provides the uniform mechanism needed to support such APIs.
As this is a new type in the std
namespace, this change does not break any existing code, except in the
unlikely event that a user has added their own
consteval_hash type to
std, which is already undefined
behavior.
This proposal requires that a given reflection hashes to the same value in every translation unit of a program, so a hash value baked into a header means the same thing everywhere it is included.
Since translation units are compiled separately, meeting that requirement means producing the same value on repeated compilations. The standard has no notion of repeated compilations, so we state this as a strong recommendation rather than a requirement. An implementation ignoring it could link translation units that disagree on a hash value, giving the usual ABI mismatch. We consider this acceptable: reflection already admits the same hazard, as shown in Appendix B.
This proposal applies only to compile-time programming, where undefined behavior is disallowed. Accordingly, it does not introduce additional undefined behavior into the standard.
hash<meta::info>Ultimately, the goal of this paper is to provide a robust way to hash
values of type
meta::info.
The most “obvious” way to do this is to implement hash<meta::info>,
but this introduces inconsistencies:
hash<T>,
existing specializations cannot be made
constexpr,
meaning we would end up with some specializations of hash<T>
being consteval-only, while others are runtime-only.hash. Having a
distinct type, so that hash<T>
is always runtime-only, solves this.hash_ofWe could sidestep the issues associated with hash<meta::info>
by instead providing a free function:
namespace std::meta {
consteval auto hash_of(info r) -> size_t;
}This would be defined in <meta>.
If users need to use
meta::info
as keys in compile-time hash maps, they can use it to implement their
own hash (as they must currently do for all other types). However, we do
not propose this because:
<meta>
provide fundamental details about reflections, and having a
hash_of function suggests there is a
single meaningful way of hashing, which is not the case. hash<T>
solves this by simply being understood to provide a reasonable
default implementation, not the implementation.
<meta>
also provides display_string_of,
which yields a reasonable string representation of the given reflection.
But aside from this single function, the point still holds.consteval_hash<meta::info>This brings us to the proposal in this paper. It has a few benefits:
hash_of, but
does not have the same drawback of suggesting that it is the single
meaningful way to hash
meta::info.
It carries the same semantics of a “reasonable implementation” that
hash<T>
has.hash_of.hash<T>
could be made
constexpr
for other types (which it can’t), there would still be an issue with
value consistency between runtime and compile-time for pointers, and
possibly other types. Separating runtime and compile-time hashing into
two separate types avoids this issue, as you shouldn’t expect two
hashing algorithms to give the same result.
consteval_hash<T*>
as well.hash<T>,
making compile-time map usage far more uniform.It also has a few obvious downsides:
hash<T>
is standardized, it will likely also want a corresponding consteval_hash<T>,
increasing the burden on implementers.hash<T>
for their own type, they may not care about hash salting and just
implement their hash<T>::operator()
as
constexpr,
which is both more natural and easier to use than also implementing
consteval_hash. However, this is
unlikely to be a significant concern for the standard.constexpr
functions remains awkward. Different types are required at compile-time
versus runtime, often necessitating heavy use of if consteval.
However, this is already a problem, and this proposal is not intended to
address it.Overall, we believe this is the more complete and extensible solution, so it is the one we are proposing. To see discussion on other names for this type, see Appendix A.
EWG polled that any hashing feature in this direction should require stability across translation units, with caveats for version drift. Feedback from implementers agreed that hash values should be stable, but cautioned against turning that stability into a strong ABI commitment.
Accordingly, this paper proposes to:
h(k)
depend only on k. This is inherited
from Cpp17Hash and, because the
translation unit in which the expression appears is not part of
k, it already gives exactly the
cross-translation-unit stability EWG asked for. We add a note making
that consequence explicit.consteval_hash<T>Add a new section, ConstevalHash
[constevalhash.requirements], defined analogously to
Cpp17Hash [hash.requirements]:
A type
Hmeets theConstevalHashrequirements if:
- 1 It is a function object type ([function.objects]).
- 2 It meets the
Cpp17CopyConstructibleandCpp17Destructiblerequirements.- 3 It is a consteval-only type ([basic.types.general]).
- 4 The expressions in the table below are valid and have the indicated semantics:
Given
Keyis an argument type for function objects of typeH,his a value of type (possiblyconst)H,uis an lvalue of typeKey, andkis a value of type convertible to (possiblyconst)Key:
Expression Return type Requirement h(k)size_tThe value returned shall depend only on
k.It is a constant expression.
For two different values
t1andt2, the probability thath(t1)andh(t2)compare equal should be very small, approaching1.0 / numeric_limits<size_t>::max().[ Note: Evaluations of
h(k)in different translation units of the same program yield the same value. — end note ]h(u)size_tShall not modify u.
Add a new section, “Class template consteval_hash”
[unord.consteval_hash], defined analogously to “Class template hash”
[unord.hash]. The only difference is that the proposed wording currently
makes no mention of specializations for
nullptr_t or for cv-unqualified
arithmetic, enumeration, and pointer types (which can be added
later):
Class template
consteval_hash
- 1 Each specialization of
consteval_hashis either enabled or disabled, as described below.
- (1.1) [ Note: Enabled specializations meet the
ConstevalHashrequirements, and disabled specializations do not. — end note ]- 2 If the library provides an explicit or partial specialization of
consteval_hash<Key>, that specialization is enabled except as noted otherwise, and its member functions arenoexceptexcept as noted otherwise.- 3 If
His a disabled specialization ofconsteval_hash, these values arefalse:is_default_constructible_v<H>,is_copy_constructible_v<H>,is_copy_assignable_v<H>, andis_move_assignable_v<H>. Disabled specializations ofconsteval_hashare not function object types ([function.objects]).
- (3.1) [ Note: This means that the specialization of
consteval_hashexists, but any attempt to use it as aConstevalHashis ill-formed. — end note ]- 4 An enabled specialization
consteval_hash<Key>shall:
- (4.1) Meet the
ConstevalHashrequirements withKeyas the function call argument type, theCpp17DefaultConstructiblerequirements, theCpp17CopyAssignablerequirements, and theCpp17Swappablerequirements.- (4.2) Meet the requirement that if
k1 == k2istrue,h(k1) == h(k2)is alsotrue, wherehis an object of typeconsteval_hash<Key>andk1andk2are objects of typeKey.- (4.3) Meet the requirement that the expression
h(k), wherehis an object of typeconsteval_hash<Key>andkis an object of type convertible toKey, shall not throw an exception unlessconsteval_hash<Key>is a program-defined specialization.
meta::infoAdd a new section [meta.reflection.hash]:
namespace std { template<class T> struct consteval_hash; template<> struct consteval_hash<meta::info>; }The specialization is enabled ([unord.consteval_hash]).
consteval_hash<T>?Note that consteval_hash is a
member of namespace std, not
std::meta;
only the specialization for
meta::info
is specified in the reflection clause. Which header declares the primary
template is left as an open question for LEWG:
<functional>,
alongside hash<T>,
suits the specializations discussed in Possible
extensions of the current design, none of which have anything to do
with reflection. <meta>
would also make it available.<meta>
keeps the initial addition self-contained, at the cost of requiring a
reflection header to hash an
int at
compile time.We have a mild preference for the former, but do not think the choice affects the rest of the design.
Add two new feature macros to [version.syn], one for
the new type, and one for the
meta::info
specialization:
#define __cpp_lib_consteval_hash YYYYMML // also in <meta> #define __cpp_lib_consteval_hash_meta_info YYYYMML // also in <meta>
Like with the rest of the reflection API, consteval_hash<meta::info>::operator()
can be implemented via a compiler intrinsic. For example:
template<>
struct consteval_hash<meta::info>
{
consteval consteval_hash() = default;
consteval consteval_hash(const consteval_hash<meta::info>&) = default;
consteval consteval_hash(consteval_hash<meta::info>&&) = default;
consteval auto operator()(meta::info r) const noexcept -> size_t {
return __metafunction(meta::detail::__metafn_reflection_hash, r);
}
private:
// This unused variable is here to make consteval_hash<> a
// consteval-only type.
[[maybe_unused]] const meta::info unused = ^^::;
};We are still working on a stable implementation. Feedback from GCC and Clang implementers indicates that the most practical strategy is to reuse existing mangling infrastructure to do the hashing.
[P3068] has been approved for C++26,
allowing exception throwing within
constexpr,
so marking consteval_hash<meta::info>::operator()
as noexcept
is meaningful.
consteval_hash<T>
specializationsAs mentioned above, consteval_hash<T>
can be easily extended for all other standard and builtin types that
have a specialization of std::hash<T>.
To improve initial usability and reduce unnecessary boilerplate for common cases, we could extend the scope of the original design by providing a small set of predefined specializations.
Specifically, we would propose adding predefined consteval_hash<T>
specializations for:
std::nullptr_t.These types are both widely used as keys in unordered associative containers and straightforward to support in a portable, implementation-independent manner.
Notably, extending initial support to pointer types would help
resolve the implementation experience issues highlighted in [P3372] concerning std::hash<T*>
having inconsistent values between compile-time and runtime. By
separating compile-time hashing into a distinct type, this inconsistency
is no longer a problem.
Adding support for other library types, in order to achieve feature
parity with std::hash<T>,
should be discussed in separate proposals, as not all currently
supported types are available at compile-time and some may come with
additional restrictions.
consteval_hashThere are a few other names we considered. Below is a list of them as well as the reasons we decided against them.
| Name | Comments |
stable_hash<T> |
We feel this name would be more appropriate for a
constexpr
facility that is stable across both runtime and compile-time. |
static_hash<T> |
The keyword
static in
C++ already means “compile-time” in some cases, e.g. static_assert,
however the keyword is overloaded with many other meanings, so it could
be confusing.
consteval is
the keyword for compile-time, hence
consteval_hash. |
meta_hash<T> |
Concise and clearly related to compile-time functionality. However, the word “meta” more closely relates to reflection, which is a subset of compile-time functionality, and one which compile-time hashing does not necessarily have anything to do with. |
fixed_hash<T> |
Similar to stable_hash<T>
in meaning. |
compile_time_hash<T> |
This name best describes what it does, but given that C++ already
has the
consteval
keyword to mean “compile-time-only”, this name is less consistent with
the rest of the language. |
comptime_hash<T> |
Although we love Zig, we are proposing a C++ feature! |
ct_hash<T> |
Terse, but too terse. Would you guess that it was a shortening of “compile-time hash” at first glance? |
ce_hash<T> |
Same as above. Would you guess it was a shortening of “consteval hash”? |
compile_time_only_hash<T> |
Consistent with
move_only_function, but far too
long. |
consteval_only_hash<T> |
A slight improvement on the above, but the “only” is superfluous
since
consteval
already denotes that it only works at compile-time. |
constexpr_hash<T> |
Just incorrect, implies that it is usable at runtime too. If such a
type existed, you would expect its interface to be made up of
constexpr
functions, not
consteval. |
hash<T, hashtype::compile_time> |
Rather than a new type, we could instead extend hash<T>
by providing an enum class to select what kind of hash you want. This
enum could be extended to provide even more hashes in the future. This
feels far messier, and given that it would be impossible to implement
certain hashes for certain types, it would be misleading and provide an
API that looks incomplete. |
One major point of discussion was the stability of produced hash values across translation units and repeated compiler runs. We considered three potential implementations.
This would produce a different hash value for a given object each time the compiler is run.
meta::info
as you can simply hash the pointer into the AST that the reflection
represents.We have implemented this in Clang by hashing the AST node representing the reflected object.
For a stable hash, the values produced stay the same across repeated compiler runs.
meta::info;
it essentially requires looking at the value of the reflection and
finding something to hash there, e.g. a name. (See Feedback from implementers, where
mangling is proposed as a practical route.)We attempted to find a middle ground that captures the best of both worlds. One such compromise is to ensure stability of values when recompiling the same source code, while still allowing those values to change when the source code itself changes. This approach was rejected because it is not stable across translation units.
We have two implementations of this. The first approach is more robust and relies on hashing certain properties of the underlying reflected construct. For example, for a namespace, we hash the source location. We are currently working on modifying this to create a stable hash. This can be done by relying on more stable properties of the reflected constructs; for example, using the name of a namespace rather than the source location.
The second approach is to take an otherwise unstable hash implementation and add a layer of interning: maintain a map from AST nodes (keyed by their pointer addresses) to integer IDs, assigning each ID from an incrementing counter. This yields a straightforward, semi-stable hash implementation. It also avoids the need for updates when new reflectable constructs are added to the language, since the mechanism depends solely on pointer identity rather than on the structure of the reflected entities.
During discussion of R2 of the paper, we received feedback from compiler developers working on C++26 reflection implementations in Clang, EDG, and GCC.
EDG developers expressed a strong preference for making hash values as stable as possible and as specified as possible. Their main arguments were:
Clang developers agreed that hash values should be stable across translation units and compiler runs. The preferred implementation strategy is likely to hash an existing representation such as a mangled name.
This is considered the most practical approach, but it has limitations: not every reflected entity necessarily has a defined mangling, and hash values may differ between mangling schemes, such as Itanium and Microsoft ABI manglings. These limitations were not considered fundamental blockers, but they suggest that cross-implementation stability may be difficult to specify or guarantee.
Clang developers also raised concerns about making hash stability a strong ABI commitment. In practice, once ABI guarantees are provided, users tend to rely on them, making future changes difficult. Unspecified but stable hashing may therefore be the most realistic guarantee: stable across translation units and compiler runs, but not necessarily stable across compiler versions, ABI changes, or different implementations.
As a result, we received suggestions not to specify stability guarantees, but instead to keep them implementation-defined while providing recommendations about the preferred default semantics.
Feedback from GCC confirmed the preference for implementing stable hashing using mangling capabilities.
Overall, all three implementers agree that hash values should be stable across translation units and compiler runs. However, there are different opinions on whether this stability should be mandated by the standard or left as implementation-defined behavior, with implementers informally aligning on providing stability.
As discussed, ABI concerns are a major reason for preferring a fully stable implementation. To see why, consider the following example:
// library.hpp
#include <meta>
constexpr auto buffer_size = std::consteval_hash<std::meta::info>{}(^^int) & 0xFFFF;
struct buffer
{
char data[buffer_size];
};
buffer get_important_data();// library.cpp
#include "library.hpp"
#include <print>
buffer get_important_data()
{
auto buf = buffer{};
std::print("size of buffer in library: {}\n", sizeof(buf));
return buf;
}// main.cpp
#include "library.hpp"
#include <print>
int main()
{
const auto buf = get_important_data();
std::print("size of buffer in main: {}\n", sizeof(buf));
};Here, the buffer size depends on the hash of ^^int.
With an implementation whose hash values
differ between translation units, this program produces:
size of buffer in library: 25040
size of buffer in main: 40384However, consteval_hash would not
be the first reflection facility that makes it easy to introduce such an
ABI problem. For instance, consider the above example again, but with
the buffer size given by
constexpr auto buffer_size = std::meta::members_of(^^::, std::meta::access_context::current()).size();Now the buffer size depends on how many members the global namespace has in each translation unit, which is exactly the same problem.