Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions include/fmt/enum.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,50 @@ consteval auto make_identifier_table() -> std::array<string_view, N> {
template <typename E>
inline constexpr auto identifier_table = make_identifier_table<E>();

template <typename E, size_t N = count_enumerators<E>()>
consteval auto make_identifiers() -> std::array<std::pair<E, string_view>, N> {
auto ids = std::array<std::pair<E, string_view>, N>();
auto i = size_t();
for (std::meta::info e : std::meta::enumerators_of(^^E))
ids[i++] = {std::meta::extract<E>(e), identifier(e)};
return ids;
// Returns the size of the hash table that maps enumerator values of E to
// identifiers. It is the smallest power of two that keeps the load factor at or
// below 0.5, which guarantees a free slot and therefore terminates the search.
template <typename E> consteval auto identifier_map_size() -> size_t {
auto size = size_t(1);
while (size < count_enumerators<E>() * 2) size *= 2;
return size;
}

// Returns the index of the first slot to probe for `value`. The bits of the
// underlying value are mixed because enumerator values are usually small and
// only the low bits of the hash are used.
template <typename E, size_t N = identifier_map_size<E>()>
constexpr auto identifier_slot(E value) -> size_t {
auto h = to_uint64(value);
h ^= h >> 33;
h *= 0xff51afd7ed558ccd;
h ^= h >> 33;
return static_cast<size_t>(h & (N - 1));
}

template <typename E, size_t N = identifier_map_size<E>()>
consteval auto make_identifier_map()
-> std::array<std::pair<E, string_view>, N> {
auto map = std::array<std::pair<E, string_view>, N>();
for (std::meta::info e : std::meta::enumerators_of(^^E)) {
auto value = std::meta::extract<E>(e);
for (auto i = identifier_slot(value);; i = (i + 1) & (N - 1)) {
if (map[i].second.size() != 0) {
// Keep the identifier of the first enumerator with this value.
if (map[i].first == value) break;
continue; // The slot is taken by another value; probe the next one.
}
map[i] = {value, identifier(e)};
break;
}
}
return map;
}

// Identifiers of enumerators of E in the order of declaration.
template <typename E> inline constexpr auto identifiers = make_identifiers<E>();
// Identifiers of enumerators of E in an open-addressed hash table with linear
// probing and empty string views in the free slots.
template <typename E>
inline constexpr auto identifier_map = make_identifier_map<E>();

// Returns the identifier of the first enumerator of E equal to value or an
// empty string view if there is no such enumerator.
Expand All @@ -146,10 +179,12 @@ template <typename E> constexpr auto identifier_of(E value) -> string_view {
return i < table_size ? identifier_table<E>[static_cast<size_t>(i)]
: string_view();
} else {
for (const auto& id : identifiers<E>) {
if (id.first == value) return id.second;
constexpr size_t map_size = identifier_map_size<E>();
// A free slot terminates the search and its empty identifier is the result.
for (auto i = identifier_slot(value);; i = (i + 1) & (map_size - 1)) {
const auto& entry = identifier_map<E>[i];
if (entry.second.size() == 0 || entry.first == value) return entry.second;
}
return {};
}
}

Expand Down
85 changes: 84 additions & 1 deletion test/enum-test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ enum class [[=fmt::as_identifiers]] dense { d0, d1, d2, d3, d4 };
enum class [[=fmt::as_identifiers]] holey {
h0, h1, h2, h3, h4, h5, h6 = 9
};
// 4 holes out of 11: just too sparse, formatted via a linear search.
// 4 holes out of 11: just too sparse, formatted via a hash table.
enum class [[=fmt::as_identifiers]] sparse {
s0, s1, s2, s3, s4, s5, s6 = 10
};
Expand All @@ -47,6 +47,25 @@ enum class [[=fmt::as_identifiers]] signed_enum {
minus_one = -1,
one = 1
};
// Many scattered values, exercising collisions in the hash table.
enum class [[=fmt::as_identifiers]] scattered {
a = 1, b = 17, c = 33, d = 49, e = 65, f = 81,
g = 97, h = 113, i = 129, j = 145, k = 161, l = 177
};
Comment thread
avikivity marked this conversation as resolved.
// Values that collide in the hash table: c0, c7 and c15 share a slot, and c6
// occupies the next one, so probing for c7 and c15 has to step over it.
enum class [[=fmt::as_identifiers]] collision {
c0 = 0, c6 = 6, c7 = 7, c15 = 15
};
// Values that collide in the last slot of the hash table, so the probe
// sequence wraps around to the beginning.
enum class [[=fmt::as_identifiers]] wrapping_collision {
w8 = 8, w16 = 16, w24 = 24
};
// Aliased values in an enum that is too sparse for a lookup table.
enum class [[=fmt::as_identifiers]] sparse_alias {
one = 1, dup = 1, far = 1000
};
enum class [[=fmt::as_identifiers]] extremes : int {
lowest = INT_MIN,
highest = INT_MAX
Expand Down Expand Up @@ -97,6 +116,8 @@ TEST(enum_test, format_holey_enum) {
TEST(enum_test, format_sparse_enum) {
// One more hole than holey, which is too many for a lookup table.
static_assert(fmt::detail::identifier_table_size<sparse>() == 0);
// 7 enumerators need 16 slots to keep the load factor at or below 0.5.
static_assert(fmt::detail::identifier_map_size<sparse>() == 16);
EXPECT_EQ(fmt::format("{}", sparse::s0), "s0");
EXPECT_EQ(fmt::format("{}", sparse::s5), "s5");
EXPECT_EQ(fmt::format("{}", sparse::s6), "s6");
Expand All @@ -115,6 +136,68 @@ TEST(enum_test, format_enum_with_negative_values) {
EXPECT_EQ(fmt::format("{}", static_cast<signed_enum>(2)), "2");
}

TEST(enum_test, format_scattered_enum) {
static_assert(fmt::detail::identifier_table_size<scattered>() == 0);
static_assert(fmt::detail::identifier_map_size<scattered>() == 32);
EXPECT_EQ(fmt::format("{}", scattered::a), "a");
EXPECT_EQ(fmt::format("{}", scattered::b), "b");
EXPECT_EQ(fmt::format("{}", scattered::c), "c");
EXPECT_EQ(fmt::format("{}", scattered::d), "d");
EXPECT_EQ(fmt::format("{}", scattered::e), "e");
EXPECT_EQ(fmt::format("{}", scattered::f), "f");
EXPECT_EQ(fmt::format("{}", scattered::g), "g");
EXPECT_EQ(fmt::format("{}", scattered::h), "h");
EXPECT_EQ(fmt::format("{}", scattered::i), "i");
EXPECT_EQ(fmt::format("{}", scattered::j), "j");
EXPECT_EQ(fmt::format("{}", scattered::k), "k");
EXPECT_EQ(fmt::format("{}", scattered::l), "l");
EXPECT_EQ(fmt::format("{}", static_cast<scattered>(0)), "0");
EXPECT_EQ(fmt::format("{}", static_cast<scattered>(-1)), "-1");
EXPECT_EQ(fmt::format("{}", static_cast<scattered>(999)), "999");
}

TEST(enum_test, format_enum_with_hash_collision) {
static_assert(fmt::detail::identifier_table_size<collision>() == 0);
static_assert(fmt::detail::identifier_map_size<collision>() == 8);
// Three of the four values want the same slot and the fourth takes the slot
// next to it, filling the table to its maximum load factor of 0.5.
static_assert(fmt::detail::identifier_slot(collision::c0) ==
fmt::detail::identifier_slot(collision::c7));
static_assert(fmt::detail::identifier_slot(collision::c0) ==
fmt::detail::identifier_slot(collision::c15));
static_assert(fmt::detail::identifier_slot(collision::c6) !=
fmt::detail::identifier_slot(collision::c0));
EXPECT_EQ(fmt::format("{}", collision::c0), "c0");
EXPECT_EQ(fmt::format("{}", collision::c6), "c6");
EXPECT_EQ(fmt::format("{}", collision::c7), "c7");
EXPECT_EQ(fmt::format("{}", collision::c15), "c15");
// A value that collides with the enumerators but doesn't match any of them
// is rejected after probing the whole chain.
static_assert(fmt::detail::identifier_slot(static_cast<collision>(23)) ==
fmt::detail::identifier_slot(collision::c0));
EXPECT_EQ(fmt::format("{}", static_cast<collision>(23)), "23");
}

TEST(enum_test, format_enum_with_wrapping_hash_collision) {
static_assert(fmt::detail::identifier_table_size<wrapping_collision>() == 0);
static_assert(fmt::detail::identifier_map_size<wrapping_collision>() == 8);
// All the values want the last slot, so the probe sequence wraps around.
static_assert(fmt::detail::identifier_slot(wrapping_collision::w8) == 7);
static_assert(fmt::detail::identifier_slot(wrapping_collision::w16) == 7);
static_assert(fmt::detail::identifier_slot(wrapping_collision::w24) == 7);
EXPECT_EQ(fmt::format("{}", wrapping_collision::w8), "w8");
EXPECT_EQ(fmt::format("{}", wrapping_collision::w16), "w16");
EXPECT_EQ(fmt::format("{}", wrapping_collision::w24), "w24");
EXPECT_EQ(fmt::format("{}", static_cast<wrapping_collision>(33)), "33");
}

TEST(enum_test, format_sparse_enum_alias) {
// The first enumerator with a matching value is used in the hash table too.
static_assert(fmt::detail::identifier_table_size<sparse_alias>() == 0);
EXPECT_EQ(fmt::format("{}", sparse_alias::dup), "one");
EXPECT_EQ(fmt::format("{}", sparse_alias::far), "far");
}

TEST(enum_test, format_enum_with_extreme_values) {
// The span of the values overflows the underlying type, so no table is used.
static_assert(fmt::detail::identifier_table_size<extremes>() == 0);
Expand Down
Loading