Annotations for C++26 Hashing
In my recent post, I demonstrated how to use static reflection from C++26 to implement generic hash computation for custom types. Let's review the final implementation. The core of the solution is the calculate_hash() function, which iterates over sub-objects (including both base classes sub-objects and class/struct non-static data members) to compute a combined hash.
template <typename T>
concept Hashable = requires {
{ std::hash<T>{}(std::declval<T>()) } -> std::convertible_to<size_t>;
};
template <typename T>
requires std::is_class_v<T>
size_t calculate_hash(const T& obj, size_t seed = 0)
{
constexpr auto ctx = std::meta::access_context::unchecked();
static constexpr auto r_subobjects = std::define_static_array(std::meta::subobjects_of(^^T, ctx));
template for (constexpr auto r_sub : r_subobjects)
{
using Subobject_t = typename[:std::meta::type_of(r_sub):];
static_assert(Hashable<Subobject_t>, "Subobject must be hashable");
Utility::hash_combine(seed, obj.[:r_sub:])
}
return seed;
}
For a detailed explanation of the snippet above, please refer to the previous post.
To opt-in a custom class as hashable we have to provide template variable constant set to true.
template <typename T>
inline constexpr bool enabled_for_hashing_v = false;
template <typename T>
concept EnabledForHashing = enabled_for_hashing_v<T>;
template <EnabledForHashing T>
struct std::hash<T>
{
size_t operator()(const T& obj) const
{
return calculate_hash(obj);
}
};
// custom type
struct Person
{
int id;
std::string name;
};
// opt-in for hashing
template <>
inline constexpr bool enabled_for_hashing_v<Person> = true;
// now Person is hashable
static_assert(Hashable<Person>);
Currently, this generic solution is both elegant and concise. However, there's room for further improvement. Writing template<> constexpr bool enabled_for_hashing = true might seem cumbersome to some programmers. To enhance readability and simplify the solution, we could utilize another C++26 reflection feature - annotations. With a straightforward syntax, we could easily opt-in a custom type for hashing by simply writing:
struct [[=hashable]] Person {
int id;
std::string name;
};
Isn't this more elegant? We could take it a step further. If our type includes members that shouldn't be used in computing the hash value, we can simply annotate those members like this:
struct [[=hashable]] Person
{
int id;
std::string first_name;
std::string last_name;
[[=skipped_for_hash]] std::optional<std::string> cached_full_name;
};
To implement this, we need to closely examine the annotations in C++26.
Annotations in C++26
Annotations were introduced as a part of C++26 static reflection feature. Shortly speaking annotation is a compile-time value that can be associated with a code construct to which attributes can appertain.
Let's see some annotation examples:
struct Data
{
[[=42]] int value;
[[=1.2]] double ratio;
};
For annotations we can use values of structural types (values of these types can be used as non type-template parameters):
struct CompositeAnnotation {
int id;
int value;
};
struct Data
{
[[CompositeAnnotation{.id=7, .value=3}]] int annotated_member;
};
The annotations can be queried using three meta-functions from std::meta namespace:
auto is_annotation(info r) -> bool- returnstrueif the entity represented byris an annotationauto annotations_of(info r) -> vector<info>- returns a vector of reflections of annotations associated with reflected entity represented byrauto annotations_of_with_type(info r, info r_t) -> vector<info>- returns vector of reflection of annotations of typer_tassociated with the entity represented byr
Annotation values on the other hand can be extracted using std::meta::extract<T>(r) meta-function:
// strucutral type
struct Annotated_t {
int value;
friend std::ostream& operator<<(std::ostream& out, const Annotated_t& a);
};
inline constexpr Annotated_t annotated{42};
consteval Annotated_t annotated_with(int value) {
return {value};
}
struct Data
{
[[=42]] int value;
[[=annotated]] std::string name;
[[=annotated_with(43)]] int age;
//... rest of the struct
};
void describe_members_annotations()
{
constexpr auto ctx = std::meta::access_context::current();
constexpr static auto r_data_members = std::define_static_array(std::meta::nonstatic_data_members_of(^^Data, ctx));
template for (constexpr auto r_dm : r_data_members)
{
constexpr static auto annotations = std::define_static_array(std::meta::annotations_of(r_dm));
template for (constexpr auto annotation : annotations) {
constexpr auto r_annotation_type = std::meta::type_of(annotation);
constexpr auto value = std::meta::extract<typename[: r_annotation_type :]>(annotation);
std::cout << "Data member '" << std::meta::identifier_of(r_dm) << "' has annotation with value: " << value << '\n';
}
}
}
The output from describe_members_annotations is like this:
Data member 'value' has annotation with value: 42
Data member 'name' has annotation with value: Annotated_t{42}
Data member 'age' has annotation with value: Annotated_t{43}
Annotations for hashing
Let's apply our understanding of annotations to streamline generic hash calculations.
hashable annotation
First we would like to use [[=hashable]] annotation to opt-in hashing for a custom type. Let's define Hashable_t as an empty struct and then define compile time constant hashable.
struct Hashable_t {}
static inline constexpr Hashable_t hashable;
The next step involves defining a concept to verify if a custom type is annotated as hashable. We will utilize the annotations_of_with_type(r, r_t) query.
// concept that checks if a type has the Hashable annotation
template <typename T>
concept EnabledForHashing = std::meta::annotations_of_with_type(^^T, ^^Hashable_t).size() > 0;
Using this concept, we can restrict the generic hash implementation to only those types that are annotated:
template <EnabledForHashing T>
struct hash<T>
{
size_t operator()(const T& data) const
{
return calculate_hash(data);
}
};
Now, our new types can be easily opted-in for hashing:
struct [=hashable] Player : Person {
std::chrono::year_month_day birth_date;
};
Skipping members for hashing
We still have one task to implement: skipping annotated entities during the iteration process. We can introduce a struct, SkippedForHash_t, to serve as a tag used within the calculate_hash() function.
struct SkippedForHash_t {};
inline constexpr SkippedForHash_t skipped_for_hash;
When iterating over sub-objects, we can check if a base-class subobject or non-static member is annotated with SkippedForHash_t . To enhance readability, we can use a lambda as a predicate:
auto included_for_hashing = [](std::meta::info r_entity) consteval -> bool {
return std::meta::annotations_of_with_type(r_entity, ^^SkippedForHash_t).size() == 0;
};
The complete implementation of calculate_hash() now appears as follows:
template <typename T>
requires std::is_class_v<T>
size_t calculate_hash(const T& obj, size_t seed = 0)
{
constexpr auto ctx = std::meta::access_context::unchecked();
static constexpr auto r_subobjects = std::define_static_array(std::meta::subobjects_of(^^T, ctx));
auto included_for_hashing = [](std::meta::info r_entity) consteval -> bool {
return std::meta::annotations_of_with_type(r_entity, ^^SkippedForHash_t).size() == 0;
};
template for (constexpr auto r_sub : r_subobjects)
{
if constexpr (included_for_hashing(r_sub))
{
using Subobject_t = typename[:std::meta::type_of(r_sub):];
static_assert(Hashable<Subobject_t>, "Subobject must be hashable");
Utility::hash_combine(seed, obj.[:r_sub:]);
}
}
return seed;
}
Now we can use [[=skipped]] annotations for both base-classes and non-static members:
struct [[=hashable]] User : Id, Person, [[=skipped_for_hash]] ScoreMixIn<int> {
std::chrono::year_month_day registration_date;
[[=skipped_for_hash]] std::chrono::year_month_day last_login;
};
In the class above, both the Id and Person base classes, along with registration_date, are included in the hash computation. The ScoreMixIn base class and last_login are excluded as they are not relevant for hashing. The use of annotations makes this clear.
Conclusions
Static reflection already makes generic hashing in C++26 far more expressive, but annotations push it into genuinely ergonomic territory. By letting types explicitly opt-in to hashing and allowing individual members or base classes to be cleanly excluded, we get a solution that is both powerful and readable. More importantly, the intent becomes part of the code’s structure rather than hidden in template specializations or boilerplate traits. As the reflection ecosystem matures, these small but meaningful features will help modern C+ codebases move toward clearer semantics, safer defaults, and more maintainable metaprogramming patterns.
The example of code can be found at: https://godbolt.org/z/aYdcY4Y64

