Хеширование табуляции и N3980

У меня возникли проблемы с адаптированием ожидающего предложения С++ 1z N3980 от @HowardHinnant для работы с хэширование табуляции.

Ab initio computing хэширование табуляции работает так же, как и для алгоритма хэширования (Spooky, Murmur и т.д.), описанного в N3980. Это не так сложно: просто сериализуйте объект любого пользовательского типа с помощью hash_append() и пусть хеш-функция обновляет указатель на таблицу случайных чисел по мере продвижения.

Проблема возникает при попытке реализовать одно из приятных свойств хэширования табуляции: очень дешево вычислять инкрементные обновления для хеша, если объект мутирован. Для хэшей таблиц "ручной работы" один только перекомпонует хэш объектов, затронутых байтами.

Мой вопрос: как передавать инкрементные обновления объекту функции uhash<MyTabulationAlgorithm>, сохраняя истинную центральную тему N3980 (Типы не знают #)

Чтобы проиллюстрировать трудности проектирования: скажем, у меня есть пользовательский тип X с N элементами данных xi различных типов Ti

struct X 
{
   T1 x1;
   ...
   TN xN;
};

Теперь создайте объект и вычислите его хэш

X x { ... }; // initialize
std::size_t h = uhash<MyTabulationAlgorithm>(x);

Обновите один элемент и пересчитайте хэш

x.x2 = 42;
h ^= ...; // ?? I want to avoid calling uhash<>() again

Я мог бы вычислить инкрементное обновление как нечто вроде

h ^= hash_update(x.x2, start, stop); 

где [start, stop) представляет диапазон таблицы случайных чисел, которые соответствуют элементу данных x2. Однако, чтобы постепенно (то есть дешево!) Обновлять хэш для произвольных мутаций, каждый член данных должен каким-то образом знать свой собственный поддиапазон в сериализованном потоке байтов его содержащего класса. Это не похоже на дух N3980. Например, добавление новых членов данных в класс, содержащий класс, изменит макет класса и, следовательно, смещения в потоке с последовательным байтом.

Приложение: хэширование табуляции очень старое, и недавно было показано, что он имеет очень хорошие математические свойства (см. ссылку в Википедии). Он также очень популярен в сообществе программистов (компьютерные шахматы и идут, например,), где он идет под названием Хоббирование Zobrist. Там положение платы играет роль X и перемещает роль небольшого обновления (переместите кусок из своего источника в его пункт назначения, например). Было бы неплохо, если бы N3980 можно было не только адаптировать к такому хищению в виде табулирования, но также можно было бы разместить дешевые инкрементные обновления.

Ответ 1

Кажется, что вы должны это сделать, сообщив MyTabulationAlgorithm игнорировать значения всех членов класса, кроме того, что изменилось:

x.x2 = 42;
IncrementalHashAdaptor<MyTabulationAlgorithm, T2> inc{x.x2};
hash_append(inc, x);
h ^= inc;

Все, что требуется IncrementalHashAdaptor, это проверить диапазон памяти, который он передал, чтобы увидеть, включен ли в него x2:

template<class HashAlgorithm, class T>
struct IncrementalHashAdaptor
{
    T& t;
    HashAlgorithm h = {};
    bool found = false;
    void operator()(void const* key, std::size_t len) noexcept
    {
        if (/* t contained within [key, key + len) */) {
            assert(!found);
            found = true;
            char const* p = addressof(t);
            h.ignore(key, (p - key));
            h(p, sizeof(T));
            h.ignore(p + sizeof(T), len - (p - key) - sizeof(T));
        }
        else {
            h.ignore(key, len);
        }
    }
    operator std:size_t() const { assert(found); return h; }
};

Очевидно, что это будет работать только для членов, местоположение объектов которых можно определить извне и соответствует блоку памяти, переданному алгоритму хеширования; но это должно соответствовать подавляющему большинству случаев.

Вероятно, вы захотите обернуть IncrementalHashAdaptor и следующий hash_append в утилиту uhash_incremental; это остается как упражнение для читателя.

Существует знак вопроса над производительностью; предполагая, что HashAlgorithm::ignore(...) виден компилятору и он несложный, он должен хорошо оптимизировать; если этого не происходит, вы должны иметь возможность вычислить адрес байтового потока X::x2 при запуске программы, используя аналогичную стратегию.