Search code examples
c++templateshashtemplate-specializationcrtp

CRTP provide specialisation for derived classes in terms of base class for std::hash


TLDR

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.

What I am trying to do

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.

The code

The base and derived classes and the CRTP

#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 {}
};

How I want to use these

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
}

The template specialisations

// 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; }
};

What the compiler says

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;

Solution

  • 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);
    }