They say that all problems in computer science can be solved by another level of indirection.
If you are willing, we can implement Sean Parent's Runtime Polymorphism technique, which uses type erasure and a teensy bit of polymorphism to delegate to a free function. We can specialize std::hash
on the erased type.
Usage looks like this:
template<> struct MyTrait<Foo> : std::true_type{};
template<> struct MyTrait<Bar> : std::true_type{};
// ...
Foo a;
Bar b;
Bad c; // MyTrait<Bad>::value is false
std::cout << std::hash<my_hashable>{}(my_hashable{a}) << std::endl;
std::cout << std::hash<my_hashable>{}(my_hashable{b}) << std::endl;
// compiler error
//std::cout << std::hash<my_hashable>{}(my_hashable{c}) << std::endl;
Refer to Sean's talk for a deep dive on the approach, but here's the code (with my abbreviated explanation to follow).
First, our type-erasure class that holds a pointer to any T
for which there is a free function std::size_t do_hash(const T&)
class my_hashable
{
public:
template <class T>
my_hashable(T& x) : self_(std::make_shared<impl_hashable<T>>(&x))
{}
friend std::size_t do_hash(const my_hashable& x)
{
return x.self_->hash_();
}
private:
struct base_hashable
{
virtual ~base_hashable() = default;
virtual std::size_t hash_() const = 0;
}; // base_hashable
template <class T>
struct impl_hashable final : base_hashable
{
impl_hashable(T* x) : data_(x) { }
std::size_t hash_() const override
{
return do_hash(*data_); // call to free function
}
T* data_;
}; // impl_hashable
std::shared_ptr<const base_hashable> self_;
};
Next, our only specialization of std::hash
on the type-erased class:
namespace std
{
template<>
struct hash<my_hashable>
{
std::size_t operator()(const my_hashable& h) const{return do_hash(h);}
};
}
How it works:
my_hashable
is a non-templated class with no virtual methods (good).
- it's only member is
std::shared_ptr<const base_hashable> self_;
where base_hashable
is a private
abstract class that requires that children implement a function std::size_t _hash() const
impl_hashable
is the workhorse here; a templated class whose instances all derive from bash_hashable
and they all delegate their std::size_t hash_() const override
function to a free function that accepts a const T&
- when we construct a
my_hashable
with an arbitrary type T
, we take the address of T
and construct a impl_hashable<T>
with that pointer. We hide this impl_hashable
in a pointer to base class base_hashable
.
- calling
do_hash(const my_hashable&)
will delegate the call to the appropriate impl_hashable
s hash_
function via dynamic dispatch.
- we only need to specialize
std::hash
on my_hashable
and have it delegate to the my_hashable
's do_hash
friend function.
My approach deviates a bit from Sean's in that the type-erased object doesn't own the T
we give to it, but rather takes a non-owning pointer to a pre-existing one. This will allow you to construct a (lightweight) my_hashable
only when you need it.
Now we can define our free function that will only work for types where MyTrait<T>::value
is true
:
template<class T>
std::size_t do_hash(const T& t)
{
static_assert(MyTrait<T>::value, "Can only call do_hash for types for which MyTrait is true");
return std::hash<typename T::data_t>{}(t.data);
}
Then, as I showed in the start of this post, we can define our classes and decide which ones satisfy the trait. Here, T
takes on types of Foo
and Bar
(not my_hashable
since we already delegated to impl_hashable
which recovers the type we passed in when we constructed the my_hashable
instance)