tagged_dispatch
Memory-efficient trait dispatch using tagged pointers. Like enum_dispatch, but uses only 8 bytes per instance with heap-allocated variants instead of stack-allocated ones the size of the largest variant.
Features
- 8-byte enums - Constant size regardless of variant types
- Zero-cost dispatch - Inlined, no vtable overhead
- No allocator required - Works with
no_std(bring your own allocator) - Cache-friendly - Better locality than fat enums
- Arena allocation support - Optional arena allocation for even better performance
- Apple Silicon optimized - Leverages ARM64 TBI for zero-cost tag removal
Installation
Add this to your Cargo.toml:
[]
= "0.1"
# Optional: Enable arena allocation support
= { = "0.1", = ["allocator-bumpalo"] }
Feature Flags
std(default): Standard library supportallocator-bumpalo: ImplementsTaggedAllocatorforbumpalo::Bumpallocator-typed-arena: ImplementsTaggedAllocatorfortyped_arena::Arena<T>all-allocators: Enables all allocator implementations
Quick Example
use tagged_dispatch;
// Define your trait
// Create an enum with variants that implement the trait
// Implement the trait for each variant
// Create shapes using generated constructors
let shapes = vec!;
// Dispatch trait methods
for shape in &shapes
// Only 8 bytes per enum, not size_of::<largest variant>()!
assert_eq!;
When to Use
Use tagged_dispatch when:
- You have many instances and memory usage is critical (8 bytes vs potentially hundreds)
- Your variants are large or vary significantly in size
- You can accept the heap allocation overhead
- You want better cache locality for collections
Use enum_dispatch when:
- You want stack allocation and no heap overhead
- Your variants are similarly sized or small
- You have fewer instances
- You need the absolute fastest dispatch (no pointer indirection)
Use trait objects when:
- You need open sets of types (not known at compile time)
- You're okay with 16-byte fat pointers
- You need to work with external types you don't control
Memory Models
Owned Mode (Default)
Without lifetime parameters on the enum, generates owned tagged pointers using Box:
- Variants are allocated with
Box::into_raw(Box::new(value)) - Implements
Dropto deallocate - Has non-trivial
Clonethat deep-copies
Arena Mode
With lifetime parameters on the enum, generates arena-allocated pointers:
- Variants allocated through
TaggedAllocatortrait - Types are
Copy(just copies the 8-byte pointer) - Arena manages object lifetimes
- Variants don't need to be
Send,Sync, or evenSized
Advanced Features
Arena Allocation
For high-performance scenarios, use arena allocation to get Copy types and eliminate individual allocations:
Multiple Trait Dispatch
Dispatch multiple traits through the same enum:
use tagged_dispatch;
// Complete the example with struct definitions
// Example usage
let shape = circle;
shape.draw;
assert_eq!;
Default Implementations
Traits with default implementations work as expected:
use tagged_dispatch;
;
;
// Example usage
let dog = dog;
assert_eq!;
assert_eq!; // Uses default
let bird = bird;
assert_eq!;
assert_eq!; // Overridden
Non-Dispatched Methods
Mark trait methods that shouldn't be dispatched with #[no_dispatch]:
use tagged_dispatch;
;
;
// Example usage
let val = first;
assert_eq!; // This is dispatched
// Static method is called on the concrete type, not the enum
assert_eq!;
Architecture Requirements
This crate requires x86-64 or AArch64 architectures where the top 7 bits of 64-bit pointers are unused (standard on modern Linux, macOS, and Windows systems).
Platform Optimizations
Apple Silicon (macOS ARM64): This crate automatically leverages the ARM64 Top Byte Ignore (TBI) feature on Apple Silicon Macs. TBI allows the processor to automatically ignore the top byte of pointers during memory access, eliminating the need for software masking. This provides a measurable performance improvement by removing a bitwise AND operation from every pointer dereference in the dispatch path.
Limitations
- Supports up to 128 variant types (7-bit tag)
- Generic traits are not supported
- Requires heap allocation for variants (or arena allocation)
- Only works on x86-64 and AArch64 architectures
Safety
This crate uses unsafe code for tagged pointer manipulation. I've tried to carefully document and test all unsafe operations.
Safety Invariants
- Valid Pointers: All pointers stored in
TaggedPtrare valid, properly aligned, and point to initialized data - Tag Range: Tags are always within the valid range (0-127), enforced by debug assertions
- Memory Management: Proper cleanup via
Dropimplementation (in the default boxed implementation) ensures no memory leaks - Type Safety: Type safety is enforced at compile time through the macro-generated code
Unsafe Operations
The crate contains the following unsafe operations:
-
Pointer Dereferencing (
TaggedPtr::as_ref,TaggedPtr::as_mut):- Safety: Caller must ensure the pointer is valid and properly initialized
- Used by generated dispatch code to access variant data
-
Memory Deallocation (in generated
Dropimpl):- Safety: Uses
untagged_ptr()to ensure the original pointer is passed toBox::from_raw - Prevents memory leaks by properly deallocating heap-allocated variants
- Safety: Uses
-
Type Transmutation (in generated code):
- Safety: Tag values are guaranteed to map to valid enum discriminants
- Used to convert between tag values and enum variant types
-
Send/Sync Implementation:
- Safety:
TaggedPtr<T>isSend/Syncif and only ifTisSend/Sync - Preserves thread safety guarantees of the underlying types
- Safety:
All unsafe code is contained within the library implementation and is not exposed to users.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.