Search code examples
c++qttemplatesboost

Boost: how to use `boost::hash` with `std::pair`, when pair contains a custom type?


I'm trying to use the following custom unordered_map

using pair = std::pair<char, QColor>;
using cache = std::unordered_map<pair, QPixmap, boost::hash<pair>>;
cache _cache;

I defined the hash function for QColor as follows

template<>
struct std::hash<QColor>
{
    std::size_t operator()(const QColor &color) const noexcept
    {
        return std::hash<QRgb>{}(color.rgb());
    }
};

but no matter where I place it, whether it is a header or source file, I get a verbose compile time error from boost

C:\boost_1_77_0\boost\container_hash\extensions.hpp:305: error: C2665: 'boost::hash_value': none of the 3 overloads could convert all the argument types
C:\boost_1_77_0\boost/container_hash/hash.hpp(550): note: could be 'size_t boost::hash_value(const std::error_condition &)'
C:\boost_1_77_0\boost/container_hash/hash.hpp(543): note: or       'size_t boost::hash_value(const std::error_code &)'
C:\boost_1_77_0\boost/container_hash/hash.hpp(536): note: or       'size_t boost::hash_value(std::type_index)'
C:\boost_1_77_0\boost/container_hash/extensions.hpp(305): note: while trying to match the argument list '(const T)'

It is the last message of all. I think that boost's hashing function for pair doesn't see the hash function I defined. Do I need define it in the boost's namespace? And in general, what's the rule for defining the specific versions of templates? Why the rule that the templates must be defined in header files only doesn't apply here?

UPD: The structure of my project is as follows

// foo.h
#include <QtWidgets>
#include <boost/functional/hash.hpp>

#include <unordered_map>

template<>
struct std::hash<QColor>
{
    std::size_t operator()(const QColor &color) const noexcept
    {
        return std::hash<QRgb>{}(color.rgb());
    }
};

class Foo
{
private:
    using Pair = std::pair<char, QColor>;
    const QPixmap &getPixmapForPair(Pair c);

    using CharsCache = std::unordered_map<Pair, QPixmap, boost::hash<Pair>>;
    CharsCache _cache;
}
// foo.cpp
const QPixmap &Foo::getPixmapForPair(Pair c)
{
    auto it = _cache.find(c);
    if (it != _cache.end())
        return it->second;
}

Very oversimplified, but delivers the general idea.


Solution

  • boost::hash<> probably uses boost::hash_combine which uses hash_value overloads and it doesn't have one for QColor which could be a problem so I suggest that you create a specialization for std::hash<Pair> by moving the alias out of the class definition and then use boost::hash_combine directly in your operator():

    using Pair = std::pair<char, QColor>;
    
    namespace std {
        template<>
        struct hash<Pair> {
            std::size_t operator()(const Pair &p) const noexcept {
                std::size_t seed = 0;
                boost::hash_combine(seed, p.first);
                boost::hash_combine(seed, p.second.rgb());
                return seed;
            }
        };
    } // namespace std
    

    You could probably make it std::size_t seed = p.first; instead of initializing with 0 and calling hash_combine afterwards.

    You can then use the default hasher (std::hash<Pair>) when creating your map:

    class Foo {
    private:
        const QPixmap &getPixmapForPair(const Pair &c) const;
    
        using CharsCache = std::unordered_map<Pair, QPixmap>;
        CharsCache _cache;
    };
    

    Note that the function must return a value. I suggest that you throw an exception if it can't find a match:

    const QPixmap& Foo::getPixmapForPair(const Pair &c) const {
        auto it = _cache.find(c);
        if (it != _cache.end()) return it->second;
        throw std::runtime_error("getPixmapForPair"); // must return a value
    }
    

    Another option is to provide an overload for hash_value(const QColor&) instead of a specialization for std::hash<Pair>:

    std::size_t hash_value(const QColor& c) {
        return std::hash<QRgb>{}(c.rgb());
    }
    
    class Foo {
    private:
        using Pair = std::pair<char, QColor>;
        const QPixmap& getPixmapForPair(const Pair& p) const;
    
        using CharsCache = std::unordered_map<Pair, QPixmap, boost::hash<Pair>>;
        CharsCache _cache;
    };