The CppCoreGuidelines state that concepts should be specified for all template arguments. (see:T.10: Specify concepts for all template arguments) As practice for defining concepts, I am trying to build a hashtable with concepts defined for the Hash function and Key template arguments.
I want my hashtable to use two template arguments, HashFunc and Key. HashFunc should be a function object and Key should be the argument for the function object HashFunc.
That is, HashFunc(Key) should return a type convertible to size_t.
On cppreference, there is an example defining the concept Hashable. I replicated the example below:
template<typename T>
concept Hashable = requires(T a) {
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
This Hashable concept makes sense for many uses. In these cases, hash functions on object of type T are specializations std::hash<T>. However, for my purposes, I don't want to assume that the Hash will be std::hash<Key>. I would like the user to be able to provide a different hash function.
Since HashFunc and Key are so tightly bound, I don't think I can define separate concepts for HashFunc and Key. Is that correct? So I would like to define a concept HashConcept that deals with HashFunc and Key simultaneously.
So I define one concept Hash that deals with both. I try my best to define the concept so that it matches the named requirement for Hash here. The goal then is to satisfy 4 conditions. Below this list, I talk about trying to enforce these conditions.
- The return type is convertible to
std::size_t. - The hash function displays equality preservation (h(k1) == h(k1) for the duration of the program. see C++ Extensions for Ranges section 19.1.1)
- If
uis an lvalueKey, thenh(u)does not modifyu. - "The probability of
h(a)==h(b)fora!=bshould approach1.0/std::numeric_limits<std::size_t>::max()."
Does this list appear complete?
I don't believe concepts can enforce (4), and (4) will just need to be indicated in comments/documentation. I believe that concepts might be able to enforce (2) and (3), but I'm not sure how. C++ Extensions for Ranges section 19.5 defines the concepts Callable and RegularCallable, then says "Note: The distinction between Callable and RegularCallable
is purely semantic. — end note", suggesting that (2) cannot be enforced. That leaves (1) and (3).
I define a concept that enforces (1).
template<typename HashFunc, typename Key>
concept Hash = requires(HashFunc h, Key k) {
{ std::invoke(h, k) } -> std::convertible_to<std::size_t>;
};
Is this concept correct? (e.g., should I have used requires or returned a bool?) Can my concept be extended to address other requirements for hash functions, such as (2)-(4)?
Below is some example code that uses the concept. The result is to print 3 to the std::cout.
#include <functional>
#include <concepts>
#include <iostream>
template<typename HashFunc, typename Key>
concept HashConcept = requires(HashFunc h, Key k) {
{ std::invoke(h, k) } -> std::convertible_to<std::size_t>;
};
class HashFunc {
public:
std::size_t operator()(int i) {
return static_cast<size_t>(i);
}
};
template<typename Hash, typename Key>
requires HashConcept<Hash, Key>
size_t HashConceptUser(Hash h, Key k) {
return h(k);
}
int main() {
std::cout << HashConceptUser< HashFunc, int >(HashFunc{}, 3);
}