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_hashables 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)