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
u
is an lvalueKey
, thenh(u)
does not modifyu
. - "The probability of
h(a)==h(b)
fora!=b
should 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);
}
HashFunc
's call operator is private. – Siphonophoreconst Key& k
instead ofKey k
. If someone breaks this precondition (by legal or illegalconst_cast
) then, well, I guess it is almost (or actual) UB on their part. – Rubbicoconst Key& k
where? In theHashConceptUser
bysize_t HashConceptUser(Hash h, const Key& k)
? – Loudenrequires(HashFunc h, Key k)
->requires(HashFunc h, const Key& k)
. This will either ensure thatHashFunc
takes its parameter byconst&
or by value - either way, it cannot (unlessconst_cast
, but if the programmer really wants to break something, they will) change the passed argument. – Rubbicorequires
, but your suggestion appears to work. I made the change you suggested and changed theoperator()
function tostd::size_t operator()(int& i) { i += 1; return static_cast<size_t>(i); }
. The compiler caught the error as you said. – Loudenrequires
is "given" the arguments and tests some conditions given those exact arguments. When given just theconst&
, it will, well, require those tests to pass when givenconst&
. – Rubbicoh(a)==h(b)
fora!=b
should approach1.0 / std::numeric_limits<std::size_t>::max()
" - this may be impractical to enforce with concepts, but anyway - your identity hash has probability 0, and the desirable quality is really that the probability after% bucket_count()
- for any possiblebucket_count()
- doesn't significantly exceed1.0/table_size
. That can vary with implementation (e.g. GCC standard hash containers use prime bucket counts, Visual C++ uses powers of 2). – Snowinsummer