Я генерирую большое количество экземпляров класса и сохраняю их в файле std::unordered_set
. Я определил хэш-функцию и отношение равенства, и пока все работает как надо — я вставляю 10000 экземпляров с unordered_set::insert
, а могу их найти с unordered_set::find
. Все объекты не повреждены, и нет никаких намеков на повреждение памяти или какие-либо другие проблемы.
Однако, когда я продолжаю вставлять, 32769-я вставка завершается ошибкой — она не генерирует, но возвращает пару, где итератор равен == nullptr
(0x00000000). insert
определяется как:
pair<iterator, bool> insert(const value_type& Val);
и обычно *iterator
— это ключ, который я вставил, а логическое значение — true
.
Если я (после ошибки) попытаюсь найти объект, он есть в наборе; если я попытаюсь вставить его снова, он скажет мне, что он уже там; так что вставка, кажется, работала нормально. Просто возвращаемое значение равно pair<nullptr,true>
вместо pair<iterator,bool>
.
Обратите внимание, что если я вручную заполню итератор и продолжу работу в отладчике, та же проблема повторится при первой вставке после 65536, а затем при 131072 и т. д. (то есть для 2 ^15+1, 2^16+1, 2^17+1, ...) - но не в 3 * 32768+1 и т. д.
Для меня это выглядит как переполнение short
. Может у меня хэши совсем плохие и приводят к неравномерному заполнению бакетов, а на 32768 кончаются бакеты? Я не смог найти ничего более подробного о таком ограничении при поиске в Google, и я недостаточно знаю о сбалансированных деревьях или о том, что это внутри. он становится медленным и неэффективным, но он не должен отказывать.
Вопрос. Почему вставки 2^15+1, 2^16+1 и т. д. не работают и как этого избежать?
Это в Microsoft Visual Studio 2017 V15.7.1 (последняя версия от 15 мая 2018 г.). Компилятор настроен на использование правил C++ 2017, но я сомневаюсь, что это окажет какое-либо влияние.
Я не могу вставить полный код для минимально жизнеспособного решения, так как генерация объекта сложна для нескольких классов и методов и включает несколько сотен строк кода, сгенерированные хэши, очевидно, зависят от деталей объектов, и их нелегко воспроизвести в фиктивном коде.
### Обновление через один день ###: (я не могу указать это в ответе, потому что q был отложен) После обширной отладки стандартной библиотеки (включая много головоломок ), ответ @JamesPoag указывает на правильный ответ.
После вставки n
я получаю:
n load_factor max_load_factor bucket_count max_bucket_count
32766 0.999938965 1.00000000 32768 536870911 (=2^29-1)
32767 0.999969482 1.00000000 32768 536870911
32768 1.000000000 1.00000000 32768 536870911
32769 0.500000000 1.00000000 65536 536870911
неудивительно, что после 32768 вставок коэффициент загрузки достиг своего максимума. 32769-я вставка вызывает повторную хеширование в большую таблицу внутри внутреннего метода _Check_Size:
void _Check_size()
{ // grow table as needed
if (max_load_factor() < load_factor())
{ // rehash to bigger table
size_type _Newsize = bucket_count();
if (_Newsize < 512)
_Newsize *= 8; // multiply by 8
else if (_Newsize < _Vec.max_size() / 2)
_Newsize *= 2; // multiply safely by 2
_Init(_Newsize);
_Reinsert();
}
}
в конце вызывается _Reinsert()
и заполняет все 32769 ключей в новые корзины и _устанавливает все указатели _next
и _prev
соответственно. Это работает нормально.
Однако код, вызывающий эти два, выглядит следующим образом (Plist
– это название моего набора, этот код генерируется из шаблона):
_Insert_bucket(_Plist, _Where, _Bucket);
_TRY_BEGIN
_Check_size();
_CATCH_ALL
erase(_Make_iter(_Plist));
_RERAISE;
_CATCH_END
return (_Pairib(_Make_iter(_Plist), true));
}
Критическая точка находится в последней строке — для построения пары используется _Plist, но он содержит уже мертвый указатель на _next
, потому что все адреса бакетов были перестроены в _Check_size()
несколькими строками ранее. Я думаю, что это ошибка в std библиотеке — здесь ему нужно найти _Plist
в новом наборе, где он выглядит так же, но имеет валидный указатель _next
.
Простое «исправление» (проверено на работоспособность) заключается в расширении набора прямо перед критическим insert
:if (mySet.size() == mySet.bucket_count()) mySet.rehash(mySet.bucket_count() * 2);
.
### Дальнейшее обновление: ### Я долго (более 16 часов) пытался создать минимальный код, воспроизводящий проблему, но пока не смог . Я попытаюсь зарегистрировать фактически рассчитанные хэши для существующего большого кода.
Я обнаружил, что одно хэш-значение одного из ключей изменилось (непреднамеренно) между вставкой и повторным хешированием. . Это может быть основной причиной; если я перенесу перефразирование за пределы вставки, проблема исчезнет.
Я не уверен, существует ли правило, согласно которому хэши должны быть постоянными, но, вероятно, это имеет смысл, как еще вы могли бы снова найти ключ.
reinterpret_cast
? - person user541686   schedule 18.05.2018nullptr
? - person user2357112 supports Monica   schedule 18.05.2018sizeof(value_type)
? - person Sid S   schedule 18.05.2018== nullptr
? Вы не можете сравнивать итератор иnullptr_t
. - person aschepler   schedule 19.05.2018