I am trying to write a template specialisation for a bunch of derived classes only once in terms of the base class in a CRTP style framework. However, I can't get it to compile.
I have a base class and want to produce several flavours of derived classes. The base class names a few methods which would return objects of the type of the derived class. Because of this, I have been using the CRTP pattern to couple the two (C++23's "deducing this" is not an option sadly because at the time of writing most compilers don't support it yet).
I want to be able to hash these objects (so I can put them in certain STL containers), so I am trying to write a specialisation of std::hash
, but am coming unstuck. I can write a specialisation in terms of a derived class, but can't figure out how to write a general one in terms of the base class.
#include <vector>
template<typename Self>
class Base {
protected:
using my_type = int;
std::vector<my_type> bar; // <- I will be doing something special to hash this...
public:
virtual void baz(void) const = 0;
Self foo(void) const {
Self result = *dynamic_cast<const Self *>(this);
// do something with the private bar variable.
return result;
};
};
class Derived_A final : public Base<Derived_A> {
public:
void baz(void) const override {}
};
class Derived_B : public Base<Derived_B> {
public:
void baz(void) const override {}
};
int main() {
Derived_A d_a;
Derived_B d_b;
auto d_a_ = d_a.foo();
d_a.baz();
std::hash<Derived_B>{}(d_b);
std::hash<Derived_A>{}(d_a); // <-- This doesn't compile
}
// Works.
template<>
struct std::hash<Derived_B> {
std::size_t operator()(const auto &shape) const noexcept { return 0; }
};
// Not working.
template<typename Self>
struct std::hash<Base<Self>> {
std::size_t operator()(const Base<Self> &shape) const noexcept { return 0; }
};
error: call to deleted constructor of '__enum_hash<Derived_A>'
std::hash<Derived_A>{}(d_a);
^
/usr/local/opt/llvm/bin/../include/c++/v1/__functional/hash.h:643:5: note: '__enum_hash' has been explicitly marked deleted here
__enum_hash() = delete;
What you're looking for is this:
template<typename T> requires std::is_base_of_v<Base<T>, T>
struct std::hash<T> {
std::size_t operator()(T const& shape) const noexcept { return 0; }
};
You match for any T
what extends Base<T>
.
The reason why this works and not the previous one is because of the relationship between templates and inheritance - or lack thereof.
An instantiated type has no relationship with any other instantiated types. std::vector<int>
is a completely different and unrelated type compared to std::vector<float>
.
The same is true for types that have relationship between each other. So for example, std::hash<A>
and std::hash<Base<A>>
are also completely unrelated types. When a container instantiate std::hash<A>
, it will instantiate that type and only that type precisely. As your previous code did only provide hashes for std::hash<Base<A>>
, the type std::hash<A>
had to fallback to the non specialized one which caused the compilation error.
Bonus, you should change the dynamic_cast<Self const*>(this)
to static_cast<Self const*>
since you know at compile time that the conversion is valid.
If you want to be extra sure you can add such private method in base:
auto self() const& -> Self const& {
static_assert(std::is_base_of_v<Base<Self>, Self>);
return *static_cast<Self const&>(this);
}
auto self() & -> Self& {
static_assert(std::is_base_of_v<Base<Self>, Self>);
return *static_cast<Self&>(this);
}