Как комбинировать хеш-значения в C ++ 0x?

87

C ++ 0x добавляет hash<...>(...).

Однако я не смог найти hash_combineфункцию, представленную в boost . Как проще всего реализовать что-то подобное? Возможно, используя C ++ 0x xor_combine?

Нил Дж.
источник

Ответы:

93

Ну, просто сделайте так, как это сделали ребята из наддува:

template <class T>
inline void hash_combine(std::size_t& seed, const T& v)
{
    std::hash<T> hasher;
    seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);
}
Карл фон Мур
источник
26
да, это лучшее, что я мог сделать. Я не понимаю, как комитет по стандартам отклонил столь очевидное.
Neil G
13
@ Нил: Согласен. Я думаю, что простым решением для них было бы требование библиотеки иметь хеш для std::pair(или tupleдаже). Он вычислит хэш каждого элемента, а затем объединит их. (И в духе стандартной библиотеки, в определенной реализации.)
GManNickG 08
3
В стандарте упущено много очевидных вещей. Процесс интенсивной экспертной оценки затрудняет решение этих мелочей.
stinky472
15
Почему здесь эти магические числа? И разве указанное выше не зависит от машины (например, не будет ли оно отличаться на платформах x86 и x64)?
einpoklum 08
3
Есть статья, предлагающая включить сюда
hash_combine
36

Я поделюсь им здесь, так как он может быть полезен другим, ищущим это решение: начиная с ответа @KarlvonMoor , вот версия вариативного шаблона, которая является более краткой в ​​использовании, если вам нужно объединить несколько значений вместе:

inline void hash_combine(std::size_t& seed) { }

template <typename T, typename... Rest>
inline void hash_combine(std::size_t& seed, const T& v, Rest... rest) {
    std::hash<T> hasher;
    seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);
    hash_combine(seed, rest...);
}

Применение:

std::size_t h=0;
hash_combine(h, obj1, obj2, obj3);

Первоначально это было написано для реализации вариативного макроса, чтобы легко сделать хешируемые пользовательские типы (что, как мне кажется, является одним из основных способов использования hash_combineфункции):

#define MAKE_HASHABLE(type, ...) \
    namespace std {\
        template<> struct hash<type> {\
            std::size_t operator()(const type &t) const {\
                std::size_t ret = 0;\
                hash_combine(ret, __VA_ARGS__);\
                return ret;\
            }\
        };\
    }

Применение:

struct SomeHashKey {
    std::string key1;
    std::string key2;
    bool key3;
};

MAKE_HASHABLE(SomeHashKey, t.key1, t.key2, t.key3)
// now you can use SomeHashKey as key of an std::unordered_map
Маттео Италия
источник
Почему семя всегда сдвигается на 6 и 2 соответственно?
j00hi 04
5

Эту проблему также можно решить, используя вариативный шаблон следующим образом:

#include <functional>

template <typename...> struct hash;

template<typename T> 
struct hash<T> 
    : public std::hash<T>
{
    using std::hash<T>::hash;
};


template <typename T, typename... Rest>
struct hash<T, Rest...>
{
    inline std::size_t operator()(const T& v, const Rest&... rest) {
        std::size_t seed = hash<Rest...>{}(rest...);
        seed ^= hash<T>{}(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
        return seed;
    }
};

Применение:

#include <string>

int main(int,char**)
{
    hash<int, float, double, std::string> hasher;
    std::size_t h = hasher(1, 0.2f, 2.0, "Hello World!");
}

Конечно, можно было бы создать шаблонную функцию, но это могло бы вызвать неприятный вывод типа, например, hash("Hallo World!")будет вычислять хеш-значение по указателю, а не по строке. Вероятно, поэтому в стандарте используется структура.

килоальфаиндия
источник
4

Несколько дней назад я придумал немного улучшенную версию этого ответа (требуется поддержка C ++ 17):

template <typename T, typename... Rest>
void hashCombine(uint& seed, const T& v, Rest... rest)
{
    seed ^= ::qHash(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
    (hashCombine(seed, rest), ...);
}

Приведенный выше код лучше с точки зрения генерации кода. Я использовал функцию qHash из Qt в своем коде, но также можно использовать любые другие хешеры.

vt4a2h
источник
Напишите выражение свертки как, (int[]){0, (hashCombine(seed, rest), 0)...};и оно также будет работать в C ++ 11.
Анри Менке
3

Мне очень нравится подход C ++ 17 из ответа vt4a2h , однако он страдает от проблемы: Restпередается по значению, тогда как было бы более желательно передавать их по константным ссылкам (что является обязательным, если оно должно быть можно использовать с типами только для перемещения).

Вот адаптированная версия, которая по-прежнему использует выражение свертки (поэтому для нее требуется C ++ 17 или выше) и использует std::hash(вместо хеш-функции Qt):

template <typename T, typename... Rest>
void hash_combine(std::size_t& seed, const T& v, const Rest&... rest)
{
    seed ^= std::hash<T>{}(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
    (hash_combine(seed, rest), ...);
}

Для полноты: все типы, которые будут использоваться с этой версией, hash_combineдолжны иметь специализацию шаблона для hashвнедрения вstd пространство имен.

Пример:

namespace std // Inject hash for B into std::
{
    template<> struct hash<B>
    {
        std::size_t operator()(B const& b) const noexcept
        {
            std::size_t h = 0;
            cgb::hash_combine(h, b.firstMember, b.secondMember, b.andSoOn);
            return h;
        }
    };
}

Таким образом, этот тип Bв приведенном выше примере также можно использовать в другом типе A, как показано в следующем примере использования:

struct A
{
    std::string mString;
    int mInt;
    B mB;
    B* mPointer;
}

namespace std // Inject hash for A into std::
{
    template<> struct hash<A>
    {
        std::size_t operator()(A const& a) const noexcept
        {
            std::size_t h = 0;
            cgb::hash_combine(h,
                a.mString,
                a.mInt,
                a.mB, // calls the template specialization from above for B
                a.mPointer // does not call the template specialization but one for pointers from the standard template library
            );
            return h;
        }
    };
}
j00hi
источник
На мой взгляд, лучше использовать Hashаргументы шаблона стандартных контейнеров для указания вашего пользовательского хешера вместо того, чтобы вводить его в stdпространство имен.
Анри Менке
3

Ответ на vt4a2h , конечно , хорошо , но использует C ++ 17 раза выражения и не каждый способен переключиться на более новый набор инструменты легко. В приведенной ниже версии используется трюк с расширителем для имитации выражения свертки, и она также работает в C ++ 11 и C ++ 14 .

Кроме того, я отметил функцию inlineи использовал идеальную пересылку для аргументов вариативного шаблона.

template <typename T, typename... Rest>
inline void hashCombine(std::size_t &seed, T const &v, Rest &&... rest) {
    std::hash<T> hasher;
    seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
    (int[]){0, (hashCombine(seed, std::forward<Rest>(rest)), 0)...};
}

Живой пример в Compiler Explorer

Анри Менке
источник
Выглядит намного лучше, спасибо! Я, вероятно, не заботился о передаче по значению, потому что я использовал некоторые неявно разделяемые объекты, например, такие как QString.
vt4a2h