Files
verilator/src/V3HashTable.cpp
T

1165 lines
52 KiB
C++
Raw Normal View History

// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Tests for V3HashTable.h
//
// Code available from: https://verilator.org
//
//*************************************************************************
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of either the GNU Lesser General Public License Version 3
// or the Perl Artistic License Version 2.0.
// SPDX-FileCopyrightText: 2003-2026 Wilson Snyder
// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
//
//*************************************************************************
#include "V3HashTable.h"
#include "V3Error.h"
#include <array>
#include <functional>
#include <map>
#include <set>
#include <string>
#include <type_traits>
#include <utility>
namespace V3HashTableInternals {
// Entries that fill a table of the given capacity to its maximum load, so one more grows it
constexpr size_t maxLoad(size_t capacity) { return capacity * LOAD_FACTOR_NUM / LOAD_FACTOR_DEN; }
// Enough entries to grow the smallest table twice, that is, to fill four times the minimum
constexpr size_t GROWS_TWICE = maxLoad(4 * MIN_CAPACITY);
static_assert(GROWS_TWICE > maxLoad(2 * MIN_CAPACITY),
"SelfTest: 'GROWS_TWICE' must overflow a table twice the minimum");
//######################################################################
// Set key by value
void testValueKeys() {
struct Value final {
size_t m_hash = 0; // Hash of this entry
size_t m_id = 0; // Id of this entry
};
struct Hash final {
size_t operator()(const Value& value) const { return operator()(value.m_hash, 0); }
size_t operator()(size_t hash, size_t) const { return hash; }
};
struct Equal final {
bool operator()(const Value& a, const Value& b) const {
return operator()(a, b.m_hash, b.m_id);
}
bool operator()(const Value& value, size_t hash, size_t id) const {
return value.m_hash == hash && value.m_id == id;
}
};
using Set = V3HashSet<Value, Hash, Equal>;
// Entries that are added are found, entries that are not are not
{
const Value value{1, 0};
const Value equal{1, 0}; // Equal to 'value', and hashes the same
Set set;
UASSERT_SELFTEST(set.empty(), true); // Starts empty
UASSERT_SELFTEST(set.begin() == set.end(), true); // So iterates nothing
UASSERT_SELFTEST(set.find(value) == set.end(), true); // And finds nothing
const std::pair<Set::iterator, bool> added = set.insert(value);
UASSERT_SELFTEST(added.second, true); // Added, as it was absent
UASSERT_SELFTEST(added.first->m_id, value.m_id); // The iterator is at the entry
UASSERT_SELFTEST(&*added.first != &value, true); // Which is a copy of the argument
UASSERT_SELFTEST(set.size(), 1); // And is the only one
UASSERT_SELFTEST(set.empty(), false); // So the set is no longer empty
{
const Set::iterator it = set.find(value);
UASSERT_SELFTEST(it != set.end(), true); // Found by itself
UASSERT_SELFTEST(&*it, &*added.first); // At the entry held
}
{
const Set::iterator it = set.find(equal);
UASSERT_SELFTEST(it != set.end(), true); // And by an equal entry
UASSERT_SELFTEST(&*it, &*added.first); // At that same entry
}
{
const Set::iterator it = set.find(1, size_t{0});
UASSERT_SELFTEST(it != set.end(), true); // And by its parts
UASSERT_SELFTEST(&*it, &*added.first); // At that same entry again
}
UASSERT_SELFTEST(set.find(1, size_t{1}) == set.end(), true); // Hash same, id not
UASSERT_SELFTEST(set.find(2, size_t{0}) == set.end(), true); // Id same, hash not
// Adding an equal entry returns the one in the set, and does not add
const std::pair<Set::iterator, bool> again = set.insert(equal);
UASSERT_SELFTEST(again.second, false); // Not added, as an equal is present
UASSERT_SELFTEST(&*again.first, &*added.first); // At the entry already held
UASSERT_SELFTEST(set.size(), 1); // Still just the one entry
// Erasing through an iterator removes the entry it is at
{
const Set::iterator it = set.find(value);
UASSERT_SELFTEST(it != set.end(), true); // Present before the erase
set.erase(it);
}
UASSERT_SELFTEST(set.empty(), true); // Empty again
UASSERT_SELFTEST(set.find(value) == set.end(), true); // So the entry is gone
UASSERT_SELFTEST(set.contains(value), false); // As 'contains' agrees
// Erase by key removes whatever that key finds
UASSERT_SELFTEST(set.erase(value), false); // Nothing to erase, so says so
set.insert(value);
UASSERT_SELFTEST(set.erase(equal), true); // An equal key erases the entry held
UASSERT_SELFTEST(set.empty(), true); // Which leaves the set empty
// And by a key spelled as the parts of an entry
set.insert(value);
UASSERT_SELFTEST(set.erase(1, size_t{0}), true); // Those parts find and erase it
UASSERT_SELFTEST(set.empty(), true); // Empty once more
}
// Colliding entries are all kept and stay reachable, including after the table grows
{
constexpr size_t N = GROWS_TWICE;
Set set;
for (size_t i = 0; i < N; ++i) {
const std::pair<Set::iterator, bool> added = set.insert(Value{7, i});
UASSERT_SELFTEST(added.second, true); // Ids differ, so each is added
}
UASSERT_SELFTEST(set.size(), N); // All of them, in the one probe run
for (size_t i = 0; i < N; ++i) {
const Set::iterator it = set.find(size_t{7}, i);
UASSERT_SELFTEST(it != set.end(), true); // The growth lost nothing
UASSERT_SELFTEST(it->m_id, i); // And is the entry asked for
}
// Iterating visits every entry exactly once
std::array<bool, N> seen{};
size_t n = 0;
for (const Value& entry : set) {
UASSERT_SELFTEST(entry.m_hash, size_t{7}); // Only entries added
UASSERT_SELFTEST(seen[entry.m_id], false); // Each of them exactly once
seen[entry.m_id] = true;
++n;
}
UASSERT_SELFTEST(n, N); // And every one of them
}
}
//######################################################################
// Set key by pointer
void testPointerKeys() {
struct Value final {
size_t m_hash = 0; // Hash of this entry
size_t m_id = 0; // Id of this entry
};
struct Hash final {
size_t operator()(const Value* valuep) const { return operator()(valuep->m_hash, 0); }
size_t operator()(size_t hash, size_t) const { return hash; }
};
struct Equal final {
bool operator()(const Value* ap, const Value* bp) const {
return operator()(ap, bp->m_hash, bp->m_id);
}
bool operator()(const Value* valuep, size_t hash, size_t id) const {
return valuep->m_hash == hash && valuep->m_id == id;
}
};
using Set = V3HashSet<Value*, Hash, Equal>;
// Entries that are added are found, entries that are not are not
{
Value value{1, 0};
Value equal{1, 0}; // Equal to 'value', and hashes the same
Set set;
UASSERT_SELFTEST(set.empty(), true); // Starts empty
UASSERT_SELFTEST(set.begin() == set.end(), true); // So iterates nothing
UASSERT_SELFTEST(set.find(&value) == set.end(), true); // And finds nothing
const std::pair<Set::iterator, bool> added = set.insert(&value);
UASSERT_SELFTEST(added.second, true); // Added, as it was absent
UASSERT_SELFTEST(*added.first, &value); // The iterator is at the new entry
UASSERT_SELFTEST(set.size(), 1); // Which is the only one
UASSERT_SELFTEST(set.empty(), false); // So the set is no longer empty
{
const Set::iterator it = set.find(&value);
UASSERT_SELFTEST(it != set.end(), true); // Found by itself
UASSERT_SELFTEST(*it, &value); // At the entry held
}
{
const Set::iterator it = set.find(&equal);
UASSERT_SELFTEST(it != set.end(), true); // And by an equal entry
UASSERT_SELFTEST(*it, &value); // At that same entry
}
{
const Set::iterator it = set.find(1, size_t{0});
UASSERT_SELFTEST(it != set.end(), true); // And by its parts
UASSERT_SELFTEST(*it, &value); // At that same entry again
}
UASSERT_SELFTEST(set.find(1, size_t{1}) == set.end(), true); // Hash same, id not
UASSERT_SELFTEST(set.find(2, size_t{0}) == set.end(), true); // Id same, hash not
// Adding an equal entry returns the one in the set, and does not add
const std::pair<Set::iterator, bool> again = set.insert(&equal);
UASSERT_SELFTEST(again.second, false); // Not added, as an equal is present
UASSERT_SELFTEST(*again.first, &value); // The iterator is at the stored one
UASSERT_SELFTEST(set.size(), 1); // Still just the one entry
// Given the distinct but equal '&equal', 'find' still yields the stored '&value'
{
const Set::iterator it = set.find(&equal);
UASSERT_SELFTEST(it != set.end(), true); // Found, as they compare equal
UASSERT_SELFTEST(*it, &value); // But it is the stored one
UASSERT_SELFTEST(*it == &equal, false); // Not the one asked for
}
{
const Set::iterator it = set.find(&value);
UASSERT_SELFTEST(it != set.end(), true); // The entry is still there
UASSERT_SELFTEST(*it, &value); // And is the one stored
}
// Erasing the very entry held does remove it
{
const Set::iterator it = set.find(&value);
UASSERT_SELFTEST(it != set.end(), true); // Present before the erase
UASSERT_SELFTEST(*it, &value); // And is the object asked for
set.erase(it);
}
UASSERT_SELFTEST(set.empty(), true); // Empty again
UASSERT_SELFTEST(set.find(&value) == set.end(), true); // So the entry is gone
UASSERT_SELFTEST(set.contains(&value), false); // As 'contains' agrees
UASSERT_SELFTEST(set.empty(), true); // And no lookup added anything
// Erase by key makes no such check, so it removes whatever the key finds, here
// the stored '&value' when given the equal '&equal'
UASSERT_SELFTEST(set.erase(&value), false); // Nothing to erase, so says so
set.insert(&value);
UASSERT_SELFTEST(set.erase(&equal), true); // The equal key erases the stored one
UASSERT_SELFTEST(set.empty(), true); // Which leaves the set empty
UASSERT_SELFTEST(set.find(&value) == set.end(), true); // And unreachable
// And by a key spelled as the parts of an entry
set.insert(&value);
UASSERT_SELFTEST(set.erase(1, size_t{0}), true); // Those parts find and erase it
UASSERT_SELFTEST(set.empty(), true); // Empty once more
}
// Erasing leaves the other entries reachable, whichever is erased, including when
// entries with equal hashes are all in the one probe run
for (size_t erase = 0; erase < 4; ++erase) {
for (const size_t hash : {size_t{0}, size_t{7}, ~size_t{0}}) { // Wraps too
std::array<Value, 4> values{Value{hash, 0}, Value{hash, 1}, //
Value{hash, 2}, Value{hash, 3}};
Set set;
for (Value& value : values) set.insert(&value);
UASSERT_SELFTEST(set.size(), 4); // All distinct, so all added
{
const Set::iterator it = set.find(&values[erase]);
UASSERT_SELFTEST(it != set.end(), true); // The one to erase is present
UASSERT_SELFTEST(*it, &values[erase]); // And is the object held
set.erase(it);
}
UASSERT_SELFTEST(set.size(), 3); // Exactly one was erased
UASSERT_SELFTEST(set.find(&values[erase]) == set.end(), true); // That one
for (size_t i = 0; i < values.size(); ++i) {
if (i == erase) continue;
const Set::iterator it = set.find(hash, i);
UASSERT_SELFTEST(it != set.end(), true); // The others are all there
UASSERT_SELFTEST(*it, &values[i]); // Each at the entry inserted
}
}
}
// Growing leaves all entries reachable, including when their run wraps around the end
// of the table. Note this needs enough entries to actually grow, so do not reserve here.
for (const size_t hash : {size_t{0}, size_t{7}, ~size_t{0}}) {
std::array<Value, GROWS_TWICE> many{};
Set set;
for (size_t i = 0; i < many.size(); ++i) {
many[i] = Value{hash, i};
set.insert(&many[i]);
}
UASSERT_SELFTEST(set.size(), many.size()); // All of them were added
for (size_t i = 0; i < many.size(); ++i) {
const Set::iterator it = set.find(hash, i);
UASSERT_SELFTEST(it != set.end(), true); // And all survived the growth
UASSERT_SELFTEST(*it, &many[i]); // Each at the entry inserted
}
}
// Entries that collide but are not equal are both kept, and erase removes only the one
{
Value value{1, 0};
Value other{1, 1}; // Not equal to 'value', but lands on the same slot
Set set;
set.reserve(2);
set.insert(&value);
set.insert(&other);
UASSERT_SELFTEST(set.size(), 2); // Both kept, despite the collision
{
const Set::iterator it = set.find(&value);
UASSERT_SELFTEST(it != set.end(), true); // The first of the run is there
UASSERT_SELFTEST(*it, &value); // And is that entry
}
{
const Set::iterator it = set.find(&other);
UASSERT_SELFTEST(it != set.end(), true); // As is the one behind it
UASSERT_SELFTEST(*it, &other); // Which probing reached past the first
}
{
const Set::iterator it = set.find(&value);
UASSERT_SELFTEST(it != set.end(), true); // Present before the erase
UASSERT_SELFTEST(*it, &value); // And is the object held
set.erase(it);
}
UASSERT_SELFTEST(set.size(), 1); // Only one was erased
UASSERT_SELFTEST(set.find(&value) == set.end(), true); // Namely that one
{
const Set::iterator it = set.find(&other);
UASSERT_SELFTEST(it != set.end(), true); // The collider stays
UASSERT_SELFTEST(*it, &other); // Shifted back over the hole left
}
}
// Reserving avoids growing, and all entries survive either way
for (const bool doReserve : {false, true}) {
std::array<Value, 8> values{};
Set set;
if (doReserve) set.reserve(values.size());
for (size_t i = 0; i < values.size(); ++i) {
values[i] = Value{static_cast<size_t>(i * 1234567), i};
set.insert(&values[i]);
}
for (Value& value : values) {
const Set::iterator it = set.find(&value);
UASSERT_SELFTEST(it != set.end(), true); // Each entry survives
UASSERT_SELFTEST(*it, &value); // And is the one inserted
}
UASSERT_SELFTEST(set.size(), values.size()); // And none was added twice
}
// 'insertLazy' creates only on a miss, including when that grows the table
{
constexpr size_t N = GROWS_TWICE;
std::array<Value, N> values{};
Set set;
for (size_t i = 0; i < N; ++i) {
values[i] = Value{7, i}; // All of them hash the same
const std::pair<Set::iterator, bool> pair
= set.insertLazy(size_t{7}, i, [&]() -> Value* { return &values[i]; });
UASSERT_SELFTEST(pair.second, true); // Created, as it was absent
UASSERT_SELFTEST(*pair.first, &values[i]); // And is what the factory made
}
UASSERT_SELFTEST(set.size(), N); // All of them were added
// They are all present now, so nothing is created
for (size_t i = 0; i < N; ++i) {
bool created = false;
const std::pair<Set::iterator, bool> pair
= set.insertLazy(size_t{7}, i, [&]() -> Value* {
created = true; // LCOV_EXCL_START
return &values[i]; // LCOV_EXCL_STOP
});
UASSERT_SELFTEST(created, false); // The factory was never called
UASSERT_SELFTEST(pair.second, false); // As the lookup hit
UASSERT_SELFTEST(*pair.first, &values[i]); // On the stored entry
}
UASSERT_SELFTEST(set.size(), N); // And the set is unchanged
}
// Iterating visits every entry exactly once
{
std::array<Value, 20> values{};
std::array<bool, 20> seen{};
Set set;
for (size_t i = 0; i < values.size(); ++i) {
values[i] = Value{i / 2, i}; // Pairs of them collide
set.insert(&values[i]);
}
size_t n = 0;
for (Value* const valuep : set) {
UASSERT_SELFTEST(valuep->m_id < seen.size(), true); // Only entries added
UASSERT_SELFTEST(seen[valuep->m_id], false); // Each of them exactly once
seen[valuep->m_id] = true;
++n;
}
UASSERT_SELFTEST(n, values.size()); // And every one of them
}
}
//######################################################################
// Entries held by value, which are constructed and destroyed in step with the slots
void testEntryLifetime() {
size_t alive = 0; // Number of live entries, which the entries themselves count
// A test entry that is not default constructible, nor assignable, and that counts how
// many are alive. A move makes another live entry, so the count tracks the occupied
// slots however the container shuffles them about.
class NoDefault final {
size_t* m_alivep; // Where the live entries are counted
size_t m_hash; // Hash of this entry
size_t m_id; // Entries with equal ids are equal
public:
NoDefault(size_t* alivep, size_t hash, size_t id)
: m_alivep{alivep}
, m_hash{hash}
, m_id{id} {
++*m_alivep;
}
NoDefault(const NoDefault& that)
: m_alivep{that.m_alivep}
, m_hash{that.m_hash}
, m_id{that.m_id} {
++*m_alivep;
}
NoDefault(NoDefault&& that)
: m_alivep{that.m_alivep}
, m_hash{that.m_hash}
, m_id{that.m_id} {
++*m_alivep;
}
~NoDefault() { --*m_alivep; }
NoDefault& operator=(const NoDefault&) = delete;
NoDefault& operator=(NoDefault&&) = delete;
size_t hash() const { return m_hash; }
size_t id() const { return m_id; }
bool operator==(const NoDefault& that) const { return m_id == that.m_id; }
};
static_assert(!std::is_default_constructible<NoDefault>::value,
"SelfTest: 'NoDefault' must not be default constructible");
static_assert(!std::is_copy_assignable<NoDefault>::value,
"SelfTest: 'NoDefault' must not be copy assignable");
static_assert(!std::is_move_assignable<NoDefault>::value,
"SelfTest: 'NoDefault' must not be move assignable");
static_assert(std::is_move_constructible<NoDefault>::value,
"SelfTest: 'NoDefault' must be move constructible, to exercise moving");
struct Hash final {
size_t operator()(const NoDefault& value) const { return value.hash(); }
size_t operator()(size_t hash, size_t) const { return hash; }
};
struct Equal final {
bool operator()(const NoDefault& a, const NoDefault& b) const {
return operator()(a, b.hash(), b.id());
}
bool operator()(const NoDefault& a, size_t, size_t id) const { return a.id() == id; }
};
using Set = V3HashSet<NoDefault, Hash, Equal>;
constexpr size_t N = GROWS_TWICE;
UASSERT_SELFTEST(alive, 0); // Nothing built yet
{
Set set;
for (size_t i = 0; i < N; ++i) {
const std::pair<Set::iterator, bool> added = set.insert(NoDefault{&alive, 7, i});
UASSERT_SELFTEST(added.second, true); // Ids differ, so each is added
}
UASSERT_SELFTEST(set.size(), N); // All of them are in
UASSERT_SELFTEST(alive, N); // Held by exactly that many slots
// All of them hash the same, so they are all in the one probe run
for (size_t i = 0; i < N; ++i) {
const Set::iterator it = set.find(size_t{7}, i);
UASSERT_SELFTEST(it != set.end(), true); // Reachable through the run
UASSERT_SELFTEST(it->id(), i); // And is the entry asked for
}
// A rejected insert constructs no entry: the argument is only copied on a miss
{
const std::pair<Set::iterator, bool> dup = set.insert(NoDefault{&alive, 7, 0});
UASSERT_SELFTEST(dup.second, false); // Not added, as an equal is present
UASSERT_SELFTEST(dup.first->id(), 0); // The iterator is at the stored one
}
UASSERT_SELFTEST(set.size(), N); // Nothing was added
UASSERT_SELFTEST(alive, N); // And no copy of the argument was kept
{
const Set::iterator it = set.find(size_t{7}, size_t{0});
UASSERT_SELFTEST(it != set.end(), true); // Present before the erase
UASSERT_SELFTEST(it->id(), size_t{0}); // And is the entry asked for
set.erase(it);
}
UASSERT_SELFTEST(set.size(), N - 1); // One fewer entry
UASSERT_SELFTEST(alive, N - 1); // And one fewer live object
UASSERT_SELFTEST(set.contains(size_t{7}, size_t{0}), false); // Namely that one
// Erasing the rest keeps the live entries in step with the slots, all the way down.
// They all collide, so every erase shifts entries back over the hole.
for (size_t i = 1; i < N; ++i) {
{
const Set::iterator it = set.find(size_t{7}, i);
UASSERT_SELFTEST(it != set.end(), true); // Still reachable
UASSERT_SELFTEST(it->id(), i); // And is the entry asked for
set.erase(it);
}
UASSERT_SELFTEST(set.size(), N - 1 - i); // The count follows the erases
UASSERT_SELFTEST(alive, N - 1 - i); // As do the live entries
for (size_t j = i + 1; j < N; ++j) {
UASSERT_SELFTEST(set.contains(size_t{7}, j), true); // Shifted, not lost
}
}
UASSERT_SELFTEST(set.begin() == set.end(), true); // Erased down to empty
UASSERT_SELFTEST(alive, 0); // With every entry destroyed
// Clearing destroys every entry at once, and leaves a usable, empty set
for (size_t i = 0; i < N; ++i) set.insert(NoDefault{&alive, 7, i});
UASSERT_SELFTEST(alive, N); // Filled up again
set.clear();
UASSERT_SELFTEST(set.size(), 0); // Nothing left
UASSERT_SELFTEST(set.empty(), true); // Says so
UASSERT_SELFTEST(set.begin() == set.end(), true); // Iterates nothing
UASSERT_SELFTEST(alive, 0); // With every entry destroyed
UASSERT_SELFTEST(set.contains(size_t{7}, size_t{0}), false); // Nor finds any
set.clear(); // Clearing an empty set does nothing
UASSERT_SELFTEST(set.empty(), true); // Still empty
// Leave entries in the set, so its destructor has some to destroy
for (size_t i = 0; i < N; ++i) set.insert(NoDefault{&alive, 7, i});
UASSERT_SELFTEST(alive, N); // Live as the set goes out of scope
}
// The set is gone, so every entry it still held has been destroyed
UASSERT_SELFTEST(alive, 0); // Leaking none of them
// Clearing a set that never allocated a table does nothing
{
Set set;
set.clear();
UASSERT_SELFTEST(set.empty(), true); // Still empty
set.insert(NoDefault{&alive, 7, 0});
UASSERT_SELFTEST(set.size(), 1); // And still usable
}
UASSERT_SELFTEST(alive, 0); // Leaking nothing
}
//######################################################################
// A table can be moved, handing over the entries and leaving an empty table behind
void testMove() {
size_t alive = 0; // Number of live entries, which the entries themselves count
// A test entry that counts the live ones, so a move that copied an entry, dropped
// one, or destroyed one twice, shows up in the count
struct Counted final {
size_t* m_alivep; // Where the live entries are counted
size_t m_id; // Entries with equal ids are equal
Counted(size_t* alivep, size_t id)
: m_alivep{alivep}
, m_id{id} {
++*m_alivep;
}
Counted(const Counted& that)
: Counted{that.m_alivep, that.m_id} {}
Counted(Counted&& that)
: Counted{that.m_alivep, that.m_id} {}
~Counted() { --*m_alivep; }
Counted& operator=(const Counted&) = delete;
Counted& operator=(Counted&&) = delete;
};
struct Hash final {
size_t operator()(const Counted& entry) const { return operator()(entry.m_id); }
size_t operator()(size_t id) const { return id; }
};
struct Equal final {
bool operator()(const Counted& a, const Counted& b) const { return operator()(a, b.m_id); }
bool operator()(const Counted& a, size_t id) const { return a.m_id == id; }
};
using Set = V3HashSet<Counted, Hash, Equal>;
constexpr size_t N = GROWS_TWICE;
{
Set set;
for (size_t i = 0; i < N; ++i) set.insert(Counted{&alive, i});
UASSERT_SELFTEST(set.size(), N); // All of them are in
UASSERT_SELFTEST(alive, N); // Held by exactly that many slots
// Move construction takes the entries, making and destroying none
Set moved{std::move(set)};
UASSERT_SELFTEST(moved.size(), N); // Which the target now holds
UASSERT_SELFTEST(alive, N); // With no entry made or destroyed
UASSERT_SELFTEST(set.empty(), true); // And the source no longer holds them
UASSERT_SELFTEST(set.begin() == set.end(), true); // So it iterates nothing
for (size_t i = 0; i < N; ++i) {
UASSERT_SELFTEST(moved.contains(i), true); // Every entry came across
UASSERT_SELFTEST(set.contains(i), false); // And none stayed behind
}
// The moved from table is empty rather than broken, so it can be filled again
UASSERT_SELFTEST(set.insert(Counted{&alive, N}).second, true); // It took an entry
UASSERT_SELFTEST(set.size(), 1); // Which is all it holds
UASSERT_SELFTEST(set.empty(), false); // So it is no longer empty
UASSERT_SELFTEST(alive, N + 1); // And is one more live entry
// Move assignment destroys what the target held, then takes the source's
set = std::move(moved);
UASSERT_SELFTEST(set.size(), N); // The target holds the moved entries
UASSERT_SELFTEST(alive, N); // The entry it held itself was destroyed
UASSERT_SELFTEST(set.contains(N), false); // Namely that one
UASSERT_SELFTEST(moved.empty(), true); // And the source is empty again
for (size_t i = 0; i < N; ++i) UASSERT_SELFTEST(set.contains(i), true); // The rest moved
}
// Both tables are gone, so every entry either still held has been destroyed
UASSERT_SELFTEST(alive, 0); // Leaking none of them
}
//######################################################################
// Entries that cannot be copied, only moved
void testMoveOnlyEntries() {
// Only 'insertLazy' can add one of these, as 'insert' would copy it, and any copy the
// container made of an entry would stop this compiling.
class MoveOnly final {
size_t m_id; // Entries with equal ids are equal
public:
explicit MoveOnly(size_t id)
: m_id{id} {}
MoveOnly(MoveOnly&&) = default;
MoveOnly(const MoveOnly&) = delete;
MoveOnly& operator=(const MoveOnly&) = delete;
MoveOnly& operator=(MoveOnly&&) = delete;
~MoveOnly() = default;
size_t id() const { return m_id; }
};
static_assert(!std::is_copy_constructible<MoveOnly>::value,
"SelfTest: 'MoveOnly' must not be copy constructible");
static_assert(std::is_move_constructible<MoveOnly>::value,
"SelfTest: 'MoveOnly' must be move constructible");
struct Hash final {
// Every entry hashes the same, so they all end up in the one probe run
size_t operator()(const MoveOnly& value) const { return operator()(value.id()); }
size_t operator()(size_t) const { return 7; }
};
struct Equal final {
bool operator()(const MoveOnly& a, const MoveOnly& b) const {
return operator()(a, b.id());
}
bool operator()(const MoveOnly& a, size_t id) const { return a.id() == id; }
};
using Set = V3HashSet<MoveOnly, Hash, Equal>;
constexpr size_t N = GROWS_TWICE;
Set set;
for (size_t i = 0; i < N; ++i) {
const std::pair<Set::iterator, bool> added
= set.insertLazy(i, [i] { return MoveOnly{i}; });
UASSERT_SELFTEST(added.second, true); // Ids differ, so each is added
UASSERT_SELFTEST(added.first->id(), i); // Moved into the slot, never copied
}
UASSERT_SELFTEST(set.size(), N); // All of them are in
// They all collide, so erasing every other one shifts the rest back over the holes
for (size_t i = 0; i < N; i += 2) {
const Set::iterator it = set.find(i);
UASSERT_SELFTEST(it != set.end(), true); // Present before the erase
set.erase(it);
}
UASSERT_SELFTEST(set.size(), N / 2); // Half of them are gone
for (size_t i = 0; i < N; ++i) {
const bool erased = (i % 2) == 0;
UASSERT_SELFTEST(set.contains(i), !erased); // And it is the right half
}
}
//######################################################################
// Stateful functors, which the two argument constructor moves in
void testStatefulFunctors() {
// Hashing and comparison that both depend on a mask the functor holds, so the set only
// works if it keeps the instances it was handed
class Hash final {
size_t m_mask; // Only these bits of an entry matter
public:
explicit Hash(size_t mask)
: m_mask{mask} {}
size_t operator()(size_t value) const { return value & m_mask; }
};
class Equal final {
size_t m_mask; // Only these bits of an entry matter
public:
explicit Equal(size_t mask)
: m_mask{mask} {}
bool operator()(size_t a, size_t b) const { return (a & m_mask) == (b & m_mask); }
};
// Neither is default constructible, so the set cannot make its own
static_assert(!std::is_default_constructible<Hash>::value,
"SelfTest: 'Hash' must not be default constructible");
static_assert(!std::is_default_constructible<Equal>::value,
"SelfTest: 'Equal' must not be default constructible");
// Only the low two bits matter, so the 16 entries fall into 4 classes
V3HashSet<size_t, Hash, Equal> set{Hash{3}, Equal{3}};
for (size_t i = 0; i < 16; ++i) set.insert(i);
UASSERT_SELFTEST(set.size(), 4); // So the set kept the functors it was given
// Every entry finds the first one added of its class, which is the class itself
for (size_t i = 0; i < 16; ++i) {
const V3HashSet<size_t, Hash, Equal>::iterator it = set.find(i);
UASSERT_SELFTEST(it != set.end(), true); // Its class is present
UASSERT_SELFTEST(*it, (i & 3)); // Represented by the first one added
}
}
//######################################################################
// Backward shift deletion moves back exactly the entries whose probe run crosses the
// hole: an entry standing at its home position past the hole must stay put
void testBackwardShiftDeletion() {
// A test entry with an explicit hash, so probe runs can be laid out at will
struct Value final {
size_t m_hash = 0; // Hash of this entry
size_t m_id = 0; // Entries with equal ids are equal
};
struct Hash final {
size_t operator()(const Value* valuep) const { return operator()(valuep->m_hash, 0); }
size_t operator()(size_t hash, size_t) const { return hash; }
};
struct Equal final {
bool operator()(const Value* ap, const Value* bp) const {
return operator()(ap, bp->m_hash, bp->m_id);
}
bool operator()(const Value* valuep, size_t hash, size_t id) const {
return valuep->m_hash == hash && valuep->m_id == id;
}
};
using Set = V3HashSet<Value*, Hash, Equal>;
// One probe run of four entries with alternating home positions 'h' and 'h + 1',
// occupying four adjacent slots. Erasing the first leaves a hole: the second sits
// at its own home and must not be moved into it, while the third and fourth have
// their runs broken by the hole and must be moved back.
for (const size_t h : {size_t{0}, size_t{5}, ~size_t{0}}) { // Wraps too
std::array<Value, 4> values{Value{h, 0}, Value{h + 1, 1}, //
Value{h, 2}, Value{h + 1, 3}};
Set set;
for (Value& value : values) set.insert(&value);
UASSERT_SELFTEST(set.size(), 4); // The run holds all four
// Erase the entry at the head of the run
{
const Set::iterator it = set.find(h, size_t{0});
UASSERT_SELFTEST(it != set.end(), true); // Present before the erase
set.erase(it);
}
UASSERT_SELFTEST(set.size(), 3); // One fewer entry
UASSERT_SELFTEST(set.contains(h, size_t{0}), false); // Namely that one
// Whether moved back or left in place, every entry must remain reachable
for (size_t i = 1; i < values.size(); ++i) {
const Set::iterator it = set.find(values[i].m_hash, i);
UASSERT_SELFTEST(it != set.end(), true); // The shift lost nothing
UASSERT_SELFTEST(*it, &values[i]); // And moved back the right entries
}
// Erase the entry that stayed at its home position, shifting the last one again
{
const Set::iterator it = set.find(h + 1, size_t{1});
UASSERT_SELFTEST(it != set.end(), true); // Left where the first erase found it
set.erase(it);
}
UASSERT_SELFTEST(set.size(), 2); // Two are left
for (size_t i = 2; i < values.size(); ++i) {
const Set::iterator it = set.find(values[i].m_hash, i);
UASSERT_SELFTEST(it != set.end(), true); // Both still reachable
UASSERT_SELFTEST(*it, &values[i]); // And are the entries they were
}
// Iterating visits exactly the remaining entries
std::array<bool, 4> seen{};
for (const Value* const valuep : set) {
UASSERT_SELFTEST(seen[valuep->m_id], false); // Each entry once
seen[valuep->m_id] = true;
}
UASSERT_SELFTEST(seen[0], false); // Erased first
UASSERT_SELFTEST(seen[1], false); // Erased second
UASSERT_SELFTEST(seen[2], true); // Moved back over the first hole
UASSERT_SELFTEST(seen[3], true); // And back again over the second
}
}
//######################################################################
// Entries stay in place unless the table grows or an entry is erased, as only those
// two invalidate iterators
void testReferenceStability() {
struct Value final {
size_t m_hash = 0; // Hash of this entry
size_t m_id = 0; // Id of this entry
};
struct Hash final {
size_t operator()(const Value* valuep) const { return operator()(valuep->m_hash, 0); }
size_t operator()(size_t hash, size_t) const { return hash; }
};
struct Equal final {
bool operator()(const Value* ap, const Value* bp) const {
return operator()(ap, bp->m_hash, bp->m_id);
}
bool operator()(const Value* valuep, size_t hash, size_t id) const {
return valuep->m_hash == hash && valuep->m_id == id;
}
};
using Set = V3HashSet<Value*, Hash, Equal>;
// A reserved but still empty set finds nothing and iterates nothing
{
Set set;
set.reserve(8);
UASSERT_SELFTEST(set.contains(size_t{7}, size_t{0}), false); // Room, but no entry
UASSERT_SELFTEST(set.begin() == set.end(), true); // So iterates nothing
UASSERT_SELFTEST(set.empty(), true); // And holds nothing
}
// 'N' is exactly what the reservation must hold, so this also checks the boundary
// arithmetic of 'reserve' against that of the growth check. It is more than an
// unreserved table holds without growing, so the reservation is doing the work.
constexpr size_t N = maxLoad(2 * MIN_CAPACITY);
static_assert(N > maxLoad(MIN_CAPACITY), "SelfTest: 'N' must need more than a new table");
std::array<Value, N> values{};
std::array<Value* const*, N> entrypps{}; // Where each entry is stored, as inserted
Set set;
set.reserve(N);
// All entries collide, so every insertion probes through the whole existing run
for (size_t i = 0; i < N; ++i) {
values[i] = Value{7, i};
const std::pair<Set::iterator, bool> pair = set.insert(&values[i]);
UASSERT_SELFTEST(pair.second, true); // Ids differ, so each is added
entrypps[i] = &*pair.first;
}
// The set was reserved, so no insertion grew the table, and no entry has moved
for (size_t i = 0; i < N; ++i) {
const Set::iterator it = set.find(size_t{7}, i);
UASSERT_SELFTEST(it != set.end(), true); // Every entry is still there
UASSERT_SELFTEST(&*it, entrypps[i]); // In the slot it was put in
}
// Inserting entries that are present adds nothing and moves nothing
for (size_t i = 0; i < N; ++i) {
const std::pair<Set::iterator, bool> pair = set.insert(&values[i]);
UASSERT_SELFTEST(pair.second, false); // Rejected, as it is present
UASSERT_SELFTEST(&*pair.first, entrypps[i]); // And nothing moved
}
UASSERT_SELFTEST(set.size(), N); // No duplicate was added
// Growing an occupied set through 'reserve' keeps every entry
set.reserve(8 * N);
UASSERT_SELFTEST(set.size(), N); // Rehashing dropped nothing
std::array<Value* const*, N> grownpps{}; // Where each entry is after the growth
for (size_t i = 0; i < N; ++i) {
const Set::iterator it = set.find(size_t{7}, i);
UASSERT_SELFTEST(it != set.end(), true); // And left every entry reachable
grownpps[i] = &*it;
}
// Reserving room that is there already leaves the table alone, so nothing moves
set.reserve(N);
UASSERT_SELFTEST(set.size(), N); // The smaller request changed nothing
for (size_t i = 0; i < N; ++i) {
const Set::iterator it = set.find(size_t{7}, i);
UASSERT_SELFTEST(it != set.end(), true); // Every entry is still there
UASSERT_SELFTEST(&*it, grownpps[i]); // In the slot the growth left it in
}
}
//######################################################################
// A pseudo random workload checked against std::set or std::map as the reference model
// Only a few distinct hashes, so probe runs are long and every erase shifts entries
struct ClusteredHash final {
size_t operator()(size_t value) const { return value & 0x7; }
};
// Hashes spread by a large odd multiplier, so most probe runs are short
struct SpreadHash final {
size_t operator()(size_t value) const {
return static_cast<size_t>(value * 0x9e3779b97f4a7c15ULL);
}
};
// Drive a set through a deterministic pseudo random workload of insertions, erasures,
// and lookups, checking every step against a std::set holding the same entries
template <typename T_Hash>
void testSetAgainstModel() {
struct Equal final {
bool operator()(size_t a, size_t b) const { return a == b; }
};
using Set = V3HashSet<size_t, T_Hash, Equal>;
constexpr size_t UNIVERSE = 64; // Entries drawn from a small range, so lookups hit
constexpr size_t STEPS = 10000; // Number of operations applied
// A simple linear congruential generator, with a fixed seed so failures reproduce
uint64_t state = 0x123456789abcdef0ULL;
const auto nextRand = [&state]() -> size_t {
state = state * 6364136223846793005ULL + 1442695040888963407ULL;
return static_cast<size_t>(state >> 32);
};
Set set;
std::set<size_t> model;
for (size_t step = 0; step < STEPS; ++step) {
const size_t id = nextRand() % UNIVERSE;
switch (nextRand() % 4) {
case 0: { // Insert a copy
const std::pair<typename Set::iterator, bool> pair = set.insert(id);
UASSERT_SELFTEST(pair.second, model.insert(id).second); // As the model
UASSERT_SELFTEST(*pair.first, id); // At the entry asked for
break;
}
case 1: { // Insert lazily, which must create only on a miss
bool created = false;
const std::pair<typename Set::iterator, bool> pair
= set.insertLazy(id, [&]() -> size_t {
created = true;
return id;
});
UASSERT_SELFTEST(pair.second, created); // Created only on a miss
UASSERT_SELFTEST(pair.second, model.insert(id).second); // As the model
UASSERT_SELFTEST(*pair.first, id); // At the entry asked for
break;
}
case 2: { // Erase, if present
const typename Set::iterator it = set.find(id);
UASSERT_SELFTEST(it != set.end(), model.count(id) != 0); // As the model
if (it != set.end()) {
set.erase(it);
model.erase(id);
}
break;
}
default: { // Look up only
const typename Set::iterator it = set.find(id);
UASSERT_SELFTEST(it != set.end(), model.count(id) != 0); // As the model
if (it != set.end()) UASSERT_SELFTEST(*it, id); // And is the entry
break;
}
}
UASSERT_SELFTEST(set.size(), model.size()); // Every step, not just at the end
}
// Check the final content exhaustively
for (size_t id = 0; id < UNIVERSE; ++id) {
UASSERT_SELFTEST(set.contains(id), model.count(id) != 0); // Present iff modelled
}
// And that iterating visits exactly the model content, each entry once
std::array<bool, UNIVERSE> seen{};
size_t n = 0;
for (const size_t entry : set) {
UASSERT_SELFTEST(entry < UNIVERSE, true); // Nothing out of thin air
UASSERT_SELFTEST(model.count(entry) != 0, true); // Only what was inserted
UASSERT_SELFTEST(seen[entry], false); // Each entry once
seen[entry] = true;
++n;
}
UASSERT_SELFTEST(n, model.size()); // And all of them
}
//######################################################################
// A hash table used as a map, whose entries are plain key and value pairs
void testMap() {
// The key extractor means the hash and equality see only keys, never entries, so
// the standard functors do, and they are what V3HashMap defaults to
using Map = V3HashMap<std::string, int>;
using Entry = Map::Entry;
// A map entry is a plain pair, so a caller never meets an internal type
static_assert(std::is_same<Entry, std::pair<std::string, int>>::value,
"SelfTest: a map entry must be a plain pair");
constexpr size_t N = GROWS_TWICE;
Map map;
for (size_t i = 0; i < N; ++i) {
const std::string key = "key" + std::to_string(i);
// 'insertLazy' calls the factory only on a miss, so a hit builds no entry
const std::pair<Map::iterator, bool> pair
= map.insertLazy(key, [&]() -> Entry { return {key, static_cast<int>(i)}; });
UASSERT_SELFTEST(pair.second, true); // Keys differ, so each is added
}
UASSERT_SELFTEST(map.size(), N); // All of them are in
// A bare key finds the entry, with no Entry built to look it up
for (size_t i = 0; i < N; ++i) {
const Map::iterator it = map.find("key" + std::to_string(i));
UASSERT_SELFTEST(it != map.end(), true); // The key alone finds it
UASSERT_SELFTEST(it->second, static_cast<int>(i)); // With the value given
}
// A value is changed by erasing the entry and inserting it again, as an iterator
// yields a const entry
for (size_t i = 0; i < N; ++i) {
const std::string key = "key" + std::to_string(i);
map.erase(map.find(key));
UASSERT_SELFTEST(map.insert({key, static_cast<int>(i) + 100}).second, true); // Anew
}
UASSERT_SELFTEST(map.size(), N); // The same keys, so the map has not grown
for (size_t i = 0; i < N; ++i) {
const Map::iterator it = map.find("key" + std::to_string(i));
UASSERT_SELFTEST(it != map.end(), true); // Each key is still there
UASSERT_SELFTEST(it->second, static_cast<int>(i) + 100); // With its new value
}
// Adding a key that is present changes nothing
{
const std::string key = "key0";
bool created = false;
const std::pair<Map::iterator, bool> pair = map.insertLazy(key, [&]() -> Entry {
created = true; // LCOV_EXCL_START
return {key, 0}; // LCOV_EXCL_STOP
});
UASSERT_SELFTEST(created, false); // The factory was never called
UASSERT_SELFTEST(pair.second, false); // As the key is present
UASSERT_SELFTEST(pair.first->second, 100); // Holding its old value
}
UASSERT_SELFTEST(map.size(), N); // And nothing was added
// 'insert' adds a key and a value as a plain pair, as std::unordered_map::insert does
{
const std::pair<Map::iterator, bool> added = map.insert({std::string{"fresh"}, 5});
UASSERT_SELFTEST(added.second, true); // Added, as the key is new
UASSERT_SELFTEST(added.first->second, 5); // With the value given
const std::pair<Map::iterator, bool> again = map.insert({std::string{"fresh"}, 6});
UASSERT_SELFTEST(again.second, false); // Rejected, as the key is present
UASSERT_SELFTEST(again.first->second, 5); // And the value is untouched
}
UASSERT_SELFTEST(map.size(), N + 1); // Just the one entry was added
{
const Map::iterator it = map.find(std::string{"fresh"});
UASSERT_SELFTEST(it != map.end(), true); // Present before the erase
UASSERT_SELFTEST(it->second, 5); // Still holding the first value
map.erase(it);
}
UASSERT_SELFTEST(map.size(), N); // Back to the earlier content
// Erasing by key removes just that entry, and says whether it did
UASSERT_SELFTEST(map.erase(std::string{"absent"}), false); // No such key
UASSERT_SELFTEST(map.size(), N); // So nothing was erased
UASSERT_SELFTEST(map.erase(std::string{"key0"}), true); // That key is present
UASSERT_SELFTEST(map.size(), N - 1); // And just the one entry went
UASSERT_SELFTEST(map.contains(std::string{"key0"}), false); // Namely that one
for (size_t i = 1; i < N; ++i) {
UASSERT_SELFTEST(map.contains("key" + std::to_string(i)), true); // Others intact
}
}
//######################################################################
// The pseudo random workload again, run against a map and checked against std::map
// Drive a map through a deterministic pseudo random workload of insertions, erasures,
// value changes and lookups, checking every step against a std::map holding the same
template <typename T_Hash>
void testMapAgainstModel() {
struct Equal final {
bool operator()(size_t a, size_t b) const { return a == b; }
};
using Map = V3HashMap<size_t, size_t, T_Hash, Equal>;
using Entry = typename Map::Entry;
constexpr size_t UNIVERSE = 64; // Keys drawn from a small range, so lookups hit
constexpr size_t STEPS = 10000; // Number of operations applied
// A simple linear congruential generator, with a fixed seed so failures reproduce
uint64_t state = 0x0fedcba987654321ULL;
const auto nextRand = [&state]() -> size_t {
state = state * 6364136223846793005ULL + 1442695040888963407ULL;
return static_cast<size_t>(state >> 32);
};
Map map;
std::map<size_t, size_t> model;
for (size_t step = 0; step < STEPS; ++step) {
const size_t key = nextRand() % UNIVERSE;
const size_t val = step; // Distinct every step, so a stale value is visible
switch (nextRand() % 5) {
case 0: { // Insert a whole entry
const std::pair<typename Map::iterator, bool> pair = map.insert({key, val});
UASSERT_SELFTEST(pair.second, model.insert({key, val}).second); // Ditto
UASSERT_SELFTEST(pair.first->first, key); // At the key asked for
break;
}
case 1: { // Insert lazily, which must create only on a miss
bool created = false;
const std::pair<typename Map::iterator, bool> pair
= map.insertLazy(key, [&]() -> Entry {
created = true;
return {key, val};
});
UASSERT_SELFTEST(pair.second, created); // Created only on a miss
UASSERT_SELFTEST(pair.second, model.insert({key, val}).second); // Ditto
UASSERT_SELFTEST(pair.first->first, key); // At the key asked for
break;
}
case 2: { // Erase by key, if present
UASSERT_SELFTEST(map.erase(key), model.erase(key) != 0); // As model
break;
}
case 3: { // Change a value, which is erasing the entry and inserting it again
const typename Map::iterator it = map.find(key);
if (it != map.end()) {
map.erase(it);
UASSERT_SELFTEST(map.insert({key, val}).second, true); // The key is free
model[key] = val;
}
break;
}
default: { // Look up only
const typename Map::iterator it = map.find(key);
UASSERT_SELFTEST(it != map.end(), model.count(key) != 0); // As the model
if (it != map.end()) {
UASSERT_SELFTEST(it->first, key); // At the key asked for
UASSERT_SELFTEST(it->second, model.at(key)); // With its value
}
break;
}
}
UASSERT_SELFTEST(map.size(), model.size()); // Every step, not just at the end
}
// Check the final content exhaustively, values included
for (size_t key = 0; key < UNIVERSE; ++key) {
const typename Map::iterator it = map.find(key);
UASSERT_SELFTEST(it != map.end(), model.count(key) != 0); // Present iff modelled
if (it != map.end()) {
UASSERT_SELFTEST(it->second, model.at(key)); // With the right value
}
}
// And that iterating visits exactly the model content, each entry once
std::array<bool, UNIVERSE> seen{};
size_t n = 0;
for (const Entry& entry : map) {
UASSERT_SELFTEST(entry.first < UNIVERSE, true); // Nothing out of thin air
UASSERT_SELFTEST(model.count(entry.first) != 0, true); // Only what was inserted
UASSERT_SELFTEST(entry.second, model.at(entry.first)); // With the right value
UASSERT_SELFTEST(seen[entry.first], false); // Each entry once
seen[entry.first] = true;
++n;
}
UASSERT_SELFTEST(n, model.size()); // And all of them
}
void selfTest() {
testValueKeys();
testPointerKeys();
testEntryLifetime();
testMove();
testMoveOnlyEntries();
testStatefulFunctors();
testBackwardShiftDeletion();
testReferenceStability();
testSetAgainstModel<ClusteredHash>();
testSetAgainstModel<SpreadHash>();
testMap();
testMapAgainstModel<ClusteredHash>();
testMapAgainstModel<SpreadHash>();
}
} // namespace V3HashTableInternals