enum-table
enum-table is a lightweight and efficient Rust library for mapping enums to values.
It provides a fast, type-safe, and allocation-free alternative to using HashMap for enum keys,
with compile-time safety and constant-time access (O(1)).
Why use enum-table?
EnumTable<K, V, N> holds a value for every variant of K, so EnumTable::get
returns &V directly instead of the Option<&V> that HashMap::get must return.
If a value can legitimately be absent, use EnumTable<K, Option<V>, N> instead;
EnumTable's Default implementation then fills every slot with None.
- Compared to
HashMap<K, V>: no heap allocation for the table structure, better cache locality, and constructible in aconstcontext. The core has no dependency onallocorstdat all, so it works in#![no_std]environments without a global allocator. - Compared to
matchstatements: a table is data rather than code, so it can be passed around, mutated at runtime, or loaded from configuration without recompiling. - Compared to arrays (
[V; N]): works with enums whose discriminants are non-contiguous or explicitly assigned (e.g.enum E { A = 1, B = 100 }), without manually mapping variants to0..Nindices.
Installation
Add this to your Cargo.toml:
[]
= "4.0"
Requires Rust 1.85 or later.
The Enumerable Trait
EnumTable's key type must implement Enumerable, which lists every variant of the enum and gives each one an index:
pub unsafe
Enumerable is an unsafe trait: implementors must guarantee that Self has no
padding bytes and that VARIANTS lists every variant of Self exactly once, sorted
in ascending order by the unsigned bit-pattern of its in-memory representation. See
the Enumerable trait documentation for the full safety contract,
including how signed discriminants sort.
Use #[derive(Enumerable)] instead of implementing this trait by hand; it generates
a correct unsafe impl with a sorted VARIANTS array and a compile-time-computed
variant_index(), without requiring any unsafe code from you.
Safety and Memory Layout
#[derive(Enumerable)] only supports field-less (C-like) enums, which never
have padding bytes on their own. A primitive representation (e.g. #[repr(u8)])
is recommended for a stable, minimal-size layout, but not required for soundness.
#[repr(align(N))] is the exception: an alignment larger than the discriminant's
natural size can add trailing padding bytes that this crate's byte-level
comparisons would read as uninitialized memory, so the derive macro rejects it
unconditionally at compile time.
use Enumerable;
// <--- Recommended, but not required for soundness.
Usage Examples
Basic Usage
use ;
let mut table = COUNT }>from_fn;
assert_eq!;
let old_b = table.set;
assert_eq!;
assert_eq!;
const Context and et! macro
You can create EnumTable instances at compile time with zero runtime overhead using the et! macro.
This is ideal for static lookup tables.
use ;
static TABLE: COUNT }> =
et!;
const A_VAL: &str = TABLE.get_const;
assert_eq!;
Serde Support
Enable serde support by adding the serde feature:
[]
= { = "4.0", = ["serde"] }
= "1.0"
use ;
use ;
let table = COUNT }>from_fn;
let json = to_string.unwrap;
assert_eq!;
let deserialized: COUNT }> =
from_str.unwrap;
assert_eq!;
Error Handling with try_from_fn
try_from_fn builds a table from a closure that may fail per variant, stopping at
the first error:
use ;
let result = COUNT }>try_from_fn;
assert_eq!;
For other construction methods, such as creating a table from existing data structures, see the API Overview section below and the full API documentation.
API Overview
For complete API documentation, visit EnumTable on doc.rs.
Construction
EnumTable::from_fn(): Create a table by mapping each enum variant to a value.EnumTable::try_from_fn(): Create a table from a closure that may fail, stopping at the first error.EnumTable::checked_from_fn(): Create a table from a closure that may returnNone, stopping at the firstNone.EnumTable::checked_from_pairs(): Create a table from(K, V)pairs, orNoneif a variant is missing or duplicated.EnumTable::from_elem(): Create a table with the sameCopyvalue for every variant.EnumTable::default(): Create a table filled with each variant'sDefaultvalue (requiresV: Default).
Access
get(),get_mut(),set(): O(1) access to the value for a variant.get_const(),get_mut_const(),set_const():const fnequivalents, using binary search instead of O(1) lookup.Index/IndexMut(table[key], acceptingKor&K): shorthand forget/get_mut.
Transformation
map(): Transforms all values in the table, given each key and value.for_each(): Calls a function with each key and a reference to its value.for_each_mut(): Mutates all values in the table in-place, given each key and value.zip_with(): Combines two tables element-wise using a function, given each key and both values.clear(): Resets every value to itsDefault(requiresV: Default).take(): Replaces a value with itsDefaultand returns the old value (requiresV: Default).
Iterators
iter(),iter_mut(): Iterate over key-value pairs.keys(): Iterate over keys.values(),values_mut(): Iterate over values.into_iter(): Consume the table and iterate over owned key-value pairs.- Implements
Extend<(K, V)>andExtend<(&K, &V)>for updating values from an iterator.
Performance
#[derive(Enumerable)]overridesvariant_index()with a match whose arms resolve to their index at compile time, which tends to compile down to O(1) for enums with dense, sequential discriminants and to a comparison tree for sparse or custom ones — either way faster than the O(log N) binary search used by the defaultvariant_index()implementation.- The
const fnvariants (get_const, etc.) always binary search instead, sincevariant_index()cannot be called from aconst fn. - No heap allocation for the table structure, for better cache locality than
HashMap. - Tables built with the
et!macro are fully constructed at compile time.
Benchmarks
construction: building a fully populated table/map from scratch (EnumTable::from_fnvs.HashMap::new+ inserting every entry).single_get/single_set: a single lookup/update on one key, measured in isolation (also comparesgetagainst theconst fnbinary-searchget_const).bulk_get_all_variants/bulk_set_all_variants: reading/writing every variant once per iteration, representing a whole-table workload rather than a single operation.iteration: iterating over every key-value pair.
construction/EnumTable::from_fn
time: [3.7364 ns 3.7384 ns 3.7408 ns]
construction/HashMap (new + insert all)
time: [75.696 ns 75.718 ns 75.744 ns]
single_get/EnumTable::get
time: [477.00 ps 477.33 ps 477.66 ps]
single_get/EnumTable::get_const
time: [2.2253 ns 2.2265 ns 2.2281 ns]
single_get/HashMap::get
time: [6.7835 ns 6.7866 ns 6.7905 ns]
single_set/EnumTable::set
time: [3.1015 ns 3.1114 ns 3.1222 ns]
single_set/HashMap::insert
time: [9.7141 ns 9.7258 ns 9.7387 ns]
bulk_get_all_variants/EnumTable::get
time: [2.3113 ns 2.3167 ns 2.3231 ns]
bulk_get_all_variants/HashMap::get
time: [43.538 ns 43.555 ns 43.575 ns]
bulk_set_all_variants/EnumTable::set
time: [21.568 ns 21.627 ns 21.688 ns]
bulk_set_all_variants/HashMap::insert
time: [56.798 ns 56.849 ns 56.916 ns]
iteration/EnumTable::iter
time: [594.43 ps 595.08 ps 595.83 ps]
iteration/HashMap::iter
time: [3.8497 ns 3.8513 ns 3.8531 ns]
Feature Flags
default: Enablesstdandderive.derive: Enables the#[derive(Enumerable)]macro.serde: EnablesSerialize/DeserializeforEnumTable. Impliesalloc.std: Builds againststdinstead of#![no_std]. Impliesalloc.alloc: Linksalloc, required byserde.
Disabling all of the above (default-features = false) builds enum-table as #![no_std]
with no heap-allocation dependency, retaining the core EnumTable/Enumerable API.
License
Licensed under the MIT license