Общ хеш за кортежи в unordered_map / unordered_set

Защо std::unordered_map<tuple<int, int>, string> не работи веднага? Досадно е да трябва да дефинирате хеш функция за tuple<int, int>, напр.

template<> struct do_hash<tuple<int, int>>                               
{   size_t operator()(std::tuple<int, int> const& tt) const {...}  }; 

Изграждане на неподредена карта с кортежи като ключове (Matthieu M. ) показва как да автоматизирате това за boost::tuple. Има ли все пак да се направи това за c++0x кортежи без използване на променливи шаблони?

Със сигурност това трябва да е в стандарта :(


person Leo Goodstadt    schedule 18.08.2011    source източник
comment
Ако използвате C++0x, защо не използвате променливи шаблони? (или има реализация с кортежи, но без различни шаблони?)   -  person R. Martinho Fernandes    schedule 18.08.2011
comment
@RMartinho : VC++ 2010 отговаря на това описание (и изглежда, че следващата версия на VC++ също е така).   -  person ildjarn    schedule 19.08.2011
comment
Ако std::tuple се реализира чрез ръчно изписване на шаблони от 1 до N-arity (като boost::tuple), тогава предполагам, че специализацията std::hash, необходима за хеширане на кортежите, също ще трябва да бъде изписана ръчно (използвайки твърде умна предварителна обработка?) по същия начин. Ааааа!   -  person Leo Goodstadt    schedule 19.08.2011


Отговори (4)


Това работи на gcc 4.5, позволявайки на всички c++0x кортежи, съдържащи стандартни хешируеми типове, да бъдат членове на unordered_map и unordered_set без повече шум. (Поставям кода в заглавен файл и просто го включвам.)

Функцията трябва да живее в пространството на имената std, така че да бъде взета от аргумент-зависимо търсене на име (ADL).

Има ли по-просто решение?

#include <tuple>
namespace std{
    namespace
    {

        // Code from boost
        // Reciprocal of the golden ratio helps spread entropy
        //     and handles duplicates.
        // See Mike Seymour in magic-numbers-in-boosthash-combine:
        //     http://stackoverflow.com/questions/4948780

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

        // Recursive template code derived from Matthieu M.
        template <class Tuple, size_t Index = std::tuple_size<Tuple>::value - 1>
        struct HashValueImpl
        {
          static void apply(size_t& seed, Tuple const& tuple)
          {
            HashValueImpl<Tuple, Index-1>::apply(seed, tuple);
            hash_combine(seed, std::get<Index>(tuple));
          }
        };

        template <class Tuple>
        struct HashValueImpl<Tuple,0>
        {
          static void apply(size_t& seed, Tuple const& tuple)
          {
            hash_combine(seed, std::get<0>(tuple));
          }
        };
    }

    template <typename ... TT>
    struct hash<std::tuple<TT...>> 
    {
        size_t
        operator()(std::tuple<TT...> const& tt) const
        {                                              
            size_t seed = 0;                             
            HashValueImpl<std::tuple<TT...> >::apply(seed, tt);    
            return seed;                                 
        }                                              

    };
}

Стандартен код за съответствие

Yakk посочва, че специализирането на неща в пространството от имена std всъщност е недефинирано поведение. Ако желаете да имате решение, отговарящо на стандартите, тогава трябва да преместите целия този код във вашето собствено пространство от имена и да се откажете от всякаква идея ADL да намира правилното внедряване на хеш автоматично. Вместо :

unordered_set<tuple<double, int> > test_set;

Имате нужда от:

unordered_set<tuple<double, int>, hash_tuple::hash<tuple<double, int>>> test2;

където hash_tuple е вашето собствено пространство от имена, а не std::.

За да направите това, първо трябва да декларирате реализация на хеш вътре в пространството от имена hash_tuple. Това ще препрати всички типове без кортежи към std::hash:

namespace hash_tuple{

template <typename TT>
struct hash
{
    size_t
    operator()(TT const& tt) const
    {                                              
        return std::hash<TT>()(tt);                                 
    }                                              
};
}

Уверете се, че hash_combine се обажда на hash_tuple::hash, а не на std::hash

namespace hash_tuple{

namespace
    {
    template <class T>
    inline void hash_combine(std::size_t& seed, T const& v)
    {
        seed ^= hash_tuple::hash<T>()(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);
    }
}

След това включете целия друг предишен код, но го поставете вътре namespace hash_tuple, а не std::

namespace hash_tuple{

    namespace
    {
        // Recursive template code derived from Matthieu M.
        template <class Tuple, size_t Index = std::tuple_size<Tuple>::value - 1>
        struct HashValueImpl
        {
          static void apply(size_t& seed, Tuple const& tuple)
          {
            HashValueImpl<Tuple, Index-1>::apply(seed, tuple);
            hash_combine(seed, std::get<Index>(tuple));
          }
        };

        template <class Tuple>
        struct HashValueImpl<Tuple,0>
        {
          static void apply(size_t& seed, Tuple const& tuple)
          {
            hash_combine(seed, std::get<0>(tuple));
          }
        };
    }

    template <typename ... TT>
    struct hash<std::tuple<TT...>> 
    {
        size_t
        operator()(std::tuple<TT...> const& tt) const
        {                                              
            size_t seed = 0;                             
            HashValueImpl<std::tuple<TT...> >::apply(seed, tt);    
            return seed;                                 
        }                                              
    };

}
person Leo Goodstadt    schedule 18.08.2011
comment
Ако приемем, че не искате да използвате библиотеки за повишаване, не трябва ли да имате std::hash_combine вместо boost::hash_combine? - person Bo Lu; 30.07.2012
comment
Има ли std::hash_combine? - person Leo Goodstadt; 17.04.2013
comment
Вашият шаблон е компилиран с помощта на std::hash_combine с gcc 4.6.3. Направих превключването, за да избегна използването на усилване - person Bo Lu; 18.04.2013
comment
@LeoGoodstadt: +1. Вашият хеш е почти сигурно по-добър от гол XOR. Казаха ми, че seed += hash(v); seed *= m, където m е голямо нечетно число, е добър избор, но не знам дали е по-добър или по-лош от вашия. - person Marcelo Cantos; 25.04.2013
comment
@Bo Lu: Чудех се за какво говориш, тъй като включвам изрично hash_combine, за да избегна зависимостите при усилване. Тогава забелязах, че съм забравил да премахна квалификатора на boost namespace от моя код... Надявам се, че все още работи. - person Leo Goodstadt; 16.05.2013
comment
Не си струва недефинираното поведение: не специализирайте неща в std::, включващи неща, които не притежавате, и не притежавате std::tuple<TT...>. Като конкретен пример за това как това може ужасно да разбие кода, какво се случва, когато нова итерация на стандарта въведе своя собствена хеш специализация? Какво се случва, когато някой друг, който има вашата блестяща идея, въведе тясна hash<tuple<int>> специализация, която е видима от някои, но не всички места, където се използва hash<tuple<int>>? Това са конкретни примери, но UB не е ограничен от тях. Програмата ви е лошо оформена. - person Yakk - Adam Nevraumont; 04.03.2014
comment
Благодаря @Yakk. Отговорът е актуализиран, за да предложи избягване на пространството от имена std::. - person Leo Goodstadt; 20.05.2014
comment
Докосването на std не води ли до недефинирано поведение? Използването на anon namespace в std заобикаля ли това?? - person allyourcode; 28.05.2014
comment
@allyourcode Това е старо, но всъщност се препоръчва добавянето на специализации към пространството от имена на std. Добавянето на класове, функции или други дефиниции е изрично забранено. en.cppreference.com/w/cpp/language/extending_std - person Alexander Huszagh; 30.04.2017
comment
@AlexanderHuszagh Можете да добавяте специализации в пространството от имена std само за персонализирани типове (следователно не и за std::tuple). Това е, което Yakk спомена в неговия/нейния коментар. - person Daniel Langr; 01.02.2018
comment
@DanielLangr Нито моите коментари, нито коментарите на @Yakk са противоречиви. Изрично се препоръчва да добавите специализации в пространството от имена std, като правите това за типове, които вече са в стандартната библиотека, непременно ще причини проблеми. - person Alexander Huszagh; 02.02.2018
comment
вместо hash_tuple::hash да е шаблон, можете да имате единичен hash_tuple обект с шаблон operator() - person Caleth; 15.01.2021

В моя чернова на C++0x 20.8.15 казва, че хешът е специализиран за вградени типове (включително указатели, но изглежда не предполага тяхното дерефериране). Изглежда също, че е специализиран за error_code, bitset<N>, unique_ptr<T, D>, shared_ptr<T>, typeindex, string, u16string, u32string, wstring, vector<bool, Allocator> и thread::id. (удивителен списък!)

Не съм използвал C++0x variadics, така че моето форматиране вероятно е далеч, но нещо по този начин може да работи за всички кортежи.

size_t hash_combiner(size_t left, size_t right) //replacable
{ return left + 0x9e3779b9 + (right<<6) + (right>>2);}

template<int index, class...types>
struct hash_impl {
    size_t operator()(size_t a, const std::tuple<types...>& t) const {
        typedef typename std::tuple_element<index, std::tuple<types...>>::type nexttype;
        hash_impl<index-1, types...> next;
        size_t b = std::hash<nexttype>()(std::get<index>(t));
        return next(hash_combiner(a, b), t); 
    }
};
template<class...types>
struct hash_impl<0, types...> {
    size_t operator()(size_t a, const std::tuple<types...>& t) const {
        typedef typename std::tuple_element<0, std::tuple<types...>>::type nexttype;
        size_t b = std::hash<nexttype>()(std::get<0>(t));
        return hash_combiner(a, b); 
    }
};

template<class...types>
struct tuple_hash<std::tuple<types...>> {
    size_t operator()(const std::tuple<types...>& t) {
        const size_t begin = std::tuple_size<std::tuple<types...>>::value-1;
        return hash_impl<begin, types...>()(0, t);
    }
}

Тази версия всъщност се компилира и работи

Yakk забеляза, че специализирането на std::hash директно е технически неразрешено, тъй като ние специализираме стандартен библиотечен шаблон с декларация, която не зависи от дефиниран от потребителя тип.

person Mooing Duck    schedule 18.08.2011
comment
Използвайте left() ^ right(), ако не знаете какво да правите. Вижте stackoverflow.com/questions/5889238/ . Но имайте предвид, че XOR не винаги е правилният избор. Използването на обикновено събиране може да се окаже по-добро, ако очаквате кортежите да съдържат дублирани членове. Вероятно това е причината да няма стандартен хеш за кортежи. - person Alexandre C.; 18.08.2011
comment
Трябва да е пропуск, че кортежът не е хешируем. Късно е за стандарта обаче :( - person Leo Goodstadt; 19.08.2011
comment
@Лео, колкото и да е странно, няма контейнер освен вектор‹bool›, а основният_низ може да се хешира. (bitset технически контейнер ли е?) - person Mooing Duck; 19.08.2011
comment
о, чак сега забелязах: without using variadic templates? опа. Отговорът е не, не може да се направи за всички кортежи, тъй като кортежът е променлив тип шаблон. - person Mooing Duck; 19.08.2011
comment
@Alexandre Оказва се, че този преработен код изглежда почти точно като моя отговор (след преформатиране за интервали), с изключение на това, че (1) използвам различен (по-стабилен?) начин за справяне с ентропията, когато комбинирам хеш стойности и (2) не го правя трябва да изведа типа на следващата стойност на кортежа изрично с помощта на std::tuple_element, тъй като моята функция hash_combine е параметризирана на нейния тип. - person Leo Goodstadt; 18.04.2013
comment
@AlexandreC.: ^ и + са комутативни и следователно са лош избор за комбиниране на хешове. Помислете как std::unordered_set<std::tuple<int, int, …>> ще се справи с пермутации на {1, 2, …, 10}. Вместо това използвайте некомутативен комбинатор като m * left + right, където m е голямо нечетно число. - person Marcelo Cantos; 25.04.2013
comment
Специализирането на hash за тип, който не притежавате, означава, че вашата програма е неправилно оформена. И вие не притежавате всичко от hash<tuple<Ts...>>. А ^ е ужасен хеш-комбинатор. - person Yakk - Adam Nevraumont; 04.03.2014

С C++20 е възможно да се използват сгъваеми изрази и генерични ламбда за изчисляване на хеш на кортеж без рекурсия. Предпочитам да разчитам на std::hash<uintmax_t> вместо ръчно комбиниране на хешове:

#include <cinttypes>
#include <cstddef>
#include <functional>
#include <tuple>

class hash_tuple {
    template<class T>
    struct component {
        const T& value;
        component(const T& value) : value(value) {}
        uintmax_t operator,(uintmax_t n) const {
            n ^= std::hash<T>()(value);
            n ^= n << (sizeof(uintmax_t) * 4 - 1);
            return n ^ std::hash<uintmax_t>()(n);
        }
    };

public:
    template<class Tuple>
    size_t operator()(const Tuple& tuple) const {
        return std::hash<uintmax_t>()(
            std::apply([](const auto& ... xs) { return (component(xs), ..., 0); }, tuple));
    }
};

- 1 в sizeof(uintmax_t) * 4 - 1 не е задължително, но изглежда леко подобрява разпределението на хеша. Този клас може да се използва както с std::tuple, така и с std::pair.

person Vladimir Reshetnikov    schedule 12.03.2019