stable_gen_map
A single-threaded, generational map that lets you:
- insert with
&selfinstead of&mut self, - keep
&Tstable across internal resizes, - and use generational keys to avoid use-after-free bugs.
It's designed for patterns like graphs, self-referential structures, and arenas where you want to keep &T references around while still inserting new elements, and you want to be able to defer the removal of elements, such as, at the end of a videogame's frame, or turn. Great for patterns that rely on shared mutability on a single thread, and removes a lot of borrow checker struggles.
Important: This crate is intentionally single-threaded. The map types are not
Sync, and are meant to be used from a single thread only.
Nightly (optional): The
CastKeysubsystem (StableCastMap,DefaultCastKey,new_castable_key_type!) is gated behind thecastablecargo feature and requires a nightly toolchain. The core map types (StableMap,StableDerefMap) work on stable Rust out of the box.
Getting started
Add the dependency:
Or in Cargo.toml:
[]
= "0.12"
With the default features, the crate works on stable Rust — no special toolchain needed.
To enable cast keys (requires nightly):
[]
= { = "0.12", = ["castable"] }
Core types
-
StableMap<K, T>— A stable generational map storing eachTin aBox. TheBoxallocation is reused across remove/insert cycles, so a remove followed by an insert into the same slot incurs no heap traffic. This is generally what you want. -
StableDerefMap<K, Derefable>— A stable generational map where each element is a smart pointer that implementsDerefGenMapPromise. You get stable references toDeref::Target, even if the underlyingVecreallocates. This is the "advanced" variant forBox<T>,Rc<T>,Arc<T>,&T, or custom smart pointers. -
BoxStableDerefMap<K, T>— Type alias forStableDerefMap<K, Box<T>>. Preferred overStableMapif your element needs to be boxed anyway. -
StableCastMap<CK, D>(requires thecastablefeature, nightly only) — A wrapper aroundStableDerefMapthat usesCastKeyfor type-erased heterogeneous storage. Supports implicit key upcasting viaCoerceUnsized, so aDefaultCastKey<MyStruct>can be implicitly coerced toDefaultCastKey<dyn Trait>. Useful for storingBox<dyn Any>and retrieving concrete types.
Keys implement the Key trait; you can use the provided DefaultKey or define your own with the new_key_type! macro (e.g. with smaller index/generation types or map-id checking).
API overview
Insertion (all take &self)
insert(&self, value) -> (K, &T)insert_with_key(&self, f: impl FnOnce(K) -> T) -> (K, &T)try_insert_with_key(&self, f: impl FnOnce(K) -> Result<T, E>) -> Result<(K, &T), E>
Lookup
get(&self, key) -> Option<&T>get_mut(&mut self, key) -> Option<&mut T>get_by_index_only(&self, idx) -> Option<(K, &T)>— ignores generationget_by_index_only_mut(&mut self, idx) -> Option<(K, &mut T)>map[key]/map[key]—IndexandIndexMutimpls (panics on miss)
Removal and mutation (&mut self)
remove(&mut self, key) -> Option<T>clear(&mut self)retain(&mut self, f: FnMut(K, &mut T) -> bool)drain(&mut self) -> Drain— removes all elements, yielding(K, T)
Iteration
snapshot(&self) -> Vec<(K, &T)>— safe, heap-allocates oneVecsnapshot_refs(&self) -> Vec<&T>snapshot_keys(&self) -> Vec<K>iter_mut(&mut self)— yields(K, &mut T)into_iter(self)— consumes the map, yields(K, T)iter_unsafe(&self)— unsafe, zero-allocation; caller must not insert while iterating
Capacity
new() -> Selfwith_capacity(n) -> Selfreserve(&self, additional)try_reserve(&mut self, additional) -> Result<(), TryReserveError>capacity(&self) -> usizelen(&self) -> usize
Cloning
Clone::clone— safe two-phase snapshot clone (toleratesT::clonere-entering the map)clone_efficiently_mut(&mut self)— faster single-pass clone; safe because&mut selfprevents the stored type'sClonefrom mutating the map
Complexity
get/get_mut/removeare O(1).insertis O(1) amortized (O(1) unless a resize happens).
Lifetime / safety model
- You can hold
&Tfrom the map and still callinsert(which only needs&self). removeandclearneed&mut self, so you can't free elements while there are outstanding borrows — enforced by the borrow checker.- Generational keys mean stale keys simply return
Noneinstead of aliasing newly inserted elements. - When a slot's generation counter overflows, that slot is permanently retired (never reused), avoiding any possibility of aliasing.
Custom keys
Use the new_key_type! macro to create custom key types, similar to slotmap's new_key_type!:
use new_key_type;
// Basic key (same as DefaultKey):
new_key_type!
// Custom index/generation sizes (smaller memory footprint):
new_key_type!
// Key with map-id checking (keys are bound to the map that created them):
new_key_type!
// Custom sizes + map-id:
new_key_type!
The identified variant stamps a unique MapId into every key and slot, so using a key from one map on another returns None instead of silently accessing wrong data. This is useful for catching bugs in complex systems with multiple maps.
Comparison to other data structures
StableMap lives in the same space as generational arenas / slot maps, but it's aimed at a slightly different pattern: inserting with &self while keeping existing &T references alive, in a single-threaded setting where you still sometimes remove elements at well-defined points (e.g. end of a videogame frame).
| Crate | Insert API | Removals | Main focus |
|---|---|---|---|
stable_gen_map::StableMap |
fn insert(&self, T) -> (K, &T) |
Yes (&mut self) |
Stable &T across growth, single-threaded |
slotmap::SlotMap |
fn insert(&mut self, V) -> K |
Yes (&mut self) |
General-purpose generational map |
generational_arena::Arena |
fn insert(&mut self, T) -> Index |
Yes (&mut self) |
Simple arena with deletion |
slab::Slab |
fn insert(&mut self, T) -> usize |
Yes (&mut self) |
Reusable indices, pre-allocated storage |
sharded_slab::Slab |
fn insert(&self, T) -> Option<usize> |
Yes (&self) |
Concurrent slab for multi-threaded use |
When to use stable_gen_map
Use stable_gen_map when:
- You want to insert using only a shared reference (
&self), not&mut self. - You want to use patterns that do not rely heavily on ECS.
- You need to hold on to
&Tfrom the map while also inserting new elements into the same map. - You like generational keys.
- You're in a single-threaded or scoped-thread world.
- You still want to remove elements at specific points in your logic, such as:
- at the end of a videogame frame,
- at the end of a simulation tick,
- during a periodic "cleanup" or GC-like pass.
If you don't specifically need those properties, you could take a look at slotmap, slab, sharded-slab, or generational-arena.
Basic example (using StableMap)
This shows the main selling point: insert with &self and indirect references via keys.
use ;
use DefaultKey;
use StableMap;
Cast keys (heterogeneous maps, castable feature)
StableCastMap lets you store different concrete types in the same map behind Box<dyn Any>, and look them up with typed keys. Key upcasting is implicit via CoerceUnsized:
use Any;
use DefaultCastKey;
use StableBoxCastMap;
Inner keys
Every CastKey carries pointer metadata (e.g. a vtable pointer for dyn Trait) alongside the generational index and map id. The CastKey trait exposes an associated type InnerKey — this is the same key but without the pointer metadata, i.e. the type of key the backing GenMap actually uses. By default this is DefaultMapKey<Idx, Gen>.
You can extract the inner key from any cast key with inner_key(), and use it to access the map directly without needing pointer metadata:
// Insert returns a DefaultCastKey<dyn Any> (fat key with vtable metadata)
let = map.insert;
// Strip the metadata to get a DefaultMapKey<u32, u32> (thin key)
let inner = cast_key.inner_key;
// Look up using the inner key — returns &dyn Any (the map's stored target)
let val = map.get_by_inner_key.unwrap;
assert_eq!;
The inner key methods on StableCastMap:
get_by_inner_key(&self, key) -> Option<&D::Target>— shared lookup, returns the deref target directlyget_mut_by_inner_key(&mut self, key) -> Option<&mut D::Target>— mutable lookupremove_by_inner_key(&mut self, key) -> Option<D>— removal, returns the owned smart pointer
These skip the metadata reconstruction step, so they're slightly cheaper than get/get_mut/remove. They're also useful when you only have the inner key (e.g. stored in a data structure that doesn't need to know about the concrete type behind the pointer).
The InnerKey associated type is user-configurable via the CastKey trait, so custom cast key types can choose their own inner key type as long as it implements Key with matching Idx and Gen types.
License
MIT