Expand description
§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:
[dependencies]
tagged_dispatch = "0.3"
# Optional: Enable arena allocation support
tagged_dispatch = { version = "0.3", features = ["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::tagged_dispatch;
// Define your trait
#[tagged_dispatch]
trait Draw {
fn draw(&self);
fn area(&self) -> f32;
}
// Create an enum with variants that implement the trait
#[tagged_dispatch(Draw)]
enum Shape {
Circle, // Expands to Circle(Circle)
Rectangle,
Triangle,
}
// Implement the trait for each variant
#[derive(Clone)]
struct Circle { radius: f32 }
impl Draw for Circle {
fn draw(&self) {
println!("Drawing a circle with radius {}", self.radius);
}
fn area(&self) -> f32 {
std::f32::consts::PI * self.radius * self.radius
}
}
#[derive(Clone)]
struct Rectangle { width: f32, height: f32 }
impl Draw for Rectangle {
fn draw(&self) {
println!("Drawing a {}x{} rectangle", self.width, self.height);
}
fn area(&self) -> f32 {
self.width * self.height
}
}
#[derive(Clone)]
struct Triangle { base: f32, height: f32 }
impl Draw for Triangle {
fn draw(&self) {
println!("Drawing a triangle with base {} and height {}", self.base, self.height);
}
fn area(&self) -> f32 {
0.5 * self.base * self.height
}
}
// Create shapes using generated constructors
let shapes = vec![
Shape::circle(Circle { radius: 5.0 }),
Shape::rectangle(Rectangle { width: 10.0, height: 5.0 }),
Shape::triangle(Triangle { base: 8.0, height: 6.0 }),
];
// Dispatch trait methods
for shape in &shapes {
shape.draw();
println!("Area: {}", shape.area());
}
// Only 8 bytes per enum, not size_of::<largest variant>()!
assert_eq!(std::mem::size_of::<Shape>(), 8);§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:
#[cfg(feature = "allocator-bumpalo")]
{
use tagged_dispatch::tagged_dispatch;
#[tagged_dispatch]
trait Process {
fn process(&self, value: i32) -> i32;
}
#[tagged_dispatch(Process)]
enum Processor<'a> { // Note the lifetime parameter
Doubler,
Squarer,
}
#[derive(Clone)]
struct Doubler;
impl Process for Doubler {
fn process(&self, value: i32) -> i32 { value * 2 }
}
#[derive(Clone)]
struct Squarer;
impl Process for Squarer {
fn process(&self, value: i32) -> i32 { value * value }
}
// Create an arena builder
let builder = Processor::arena_builder();
// Allocate variants in the arena
let proc1 = builder.doubler(Doubler);
let proc2 = builder.squarer(Squarer);
// These are Copy and 8 bytes each.
let proc3 = proc1;
assert_eq!(proc1.process(5), 10);
assert_eq!(proc2.process(5), 25);
assert_eq!(proc3.process(5), 10);
}§Multiple Trait Dispatch
Dispatch multiple traits through the same enum:
use tagged_dispatch::tagged_dispatch;
#[tagged_dispatch]
trait Draw {
fn draw(&self);
}
#[tagged_dispatch]
trait Serialize {
fn serialize(&self) -> String;
}
#[tagged_dispatch(Draw, Serialize)]
enum Shape {
Circle, // Simplified syntax
Rectangle,
}
// Complete the example with struct definitions
#[derive(Clone)]
struct Circle { radius: f32 }
impl Draw for Circle {
fn draw(&self) {
println!("Drawing circle");
}
}
impl Serialize for Circle {
fn serialize(&self) -> String {
format!("Circle({})", self.radius)
}
}
#[derive(Clone)]
struct Rectangle { width: f32, height: f32 }
impl Draw for Rectangle {
fn draw(&self) {
println!("Drawing rectangle");
}
}
impl Serialize for Rectangle {
fn serialize(&self) -> String {
format!("Rectangle({}x{})", self.width, self.height)
}
}
// Example usage
let shape = Shape::circle(Circle { radius: 5.0 });
shape.draw();
assert_eq!(shape.serialize(), "Circle(5)");§Default Implementations
Traits with default implementations work as expected:
use tagged_dispatch::tagged_dispatch;
#[tagged_dispatch]
trait Animal {
fn make_sound(&self) -> &str;
fn legs(&self) -> u32 {
4 // Default implementation
}
}
#[tagged_dispatch(Animal)]
enum Pet {
Dog,
Bird,
}
#[derive(Clone)]
struct Dog;
impl Animal for Dog {
fn make_sound(&self) -> &str {
"Woof!"
}
// Uses default legs() implementation (4)
}
#[derive(Clone)]
struct Bird;
impl Animal for Bird {
fn make_sound(&self) -> &str {
"Tweet!"
}
fn legs(&self) -> u32 {
2 // Override default
}
}
// Example usage
let dog = Pet::dog(Dog);
assert_eq!(dog.make_sound(), "Woof!");
assert_eq!(dog.legs(), 4); // Uses default
let bird = Pet::bird(Bird);
assert_eq!(bird.make_sound(), "Tweet!");
assert_eq!(bird.legs(), 2); // Overridden§Controlling Trait Generation
By default, tagged_dispatch generates Debug, PartialEq, Eq, PartialOrd, and Ord implementations for your enum. You can opt out of these to provide custom implementations:
use tagged_dispatch::tagged_dispatch;
use std::fmt;
#[tagged_dispatch]
trait Draw {
fn draw(&self);
}
// Opt out of Debug to provide custom formatting
#[tagged_dispatch(Draw, no_debug)]
enum Shape {
Circle,
Rectangle,
}
// Implement the trait for each type
#[derive(Clone)]
struct Circle { radius: f32 }
impl Draw for Circle {
fn draw(&self) { println!("○"); }
}
#[derive(Clone)]
struct Rectangle { width: f32, height: f32 }
impl Draw for Rectangle {
fn draw(&self) { println!("▭"); }
}
impl fmt::Debug for Shape {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.tag_type() {
ShapeType::Circle => write!(f, "○ Circle"),
ShapeType::Rectangle => write!(f, "▭ Rectangle"),
}
}
}
// Available flags:
// - no_debug: Skip Debug implementation
// - no_eq: Skip PartialEq/Eq implementations
// - no_ord: Skip PartialOrd/Ord implementations
// - no_cmp: Skip all comparison traits (PartialEq, Eq, PartialOrd, Ord)
// - no_traits: Skip all automatic trait implementationsNote that all comparison traits use pointer equality, not value equality. Two instances are equal only if they point to the same object.
§Non-Dispatched Methods
Mark trait methods that shouldn’t be dispatched with #[no_dispatch]:
use tagged_dispatch::tagged_dispatch;
#[tagged_dispatch]
trait MyTrait {
fn dispatched(&self) -> i32;
#[no_dispatch]
fn not_dispatched() -> &'static str {
"This won't be dispatched"
}
}
#[tagged_dispatch(MyTrait)]
enum Value {
First,
Second,
}
#[derive(Clone)]
struct First(i32);
impl MyTrait for First {
fn dispatched(&self) -> i32 {
self.0
}
}
#[derive(Clone)]
struct Second(i32);
impl MyTrait for Second {
fn dispatched(&self) -> i32 {
self.0 * 2
}
}
// Example usage
let val = Value::first(First(5));
assert_eq!(val.dispatched(), 5); // This is dispatched
// Static method is called on the concrete type, not the enum
assert_eq!(<First as MyTrait>::not_dispatched(), "This won't be dispatched");§Migration from 0.2.x to 0.3.0
Version 0.3.0 automatically generates trait implementations that may conflict with your existing code:
// If you previously had:
impl Debug for MyEnum {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Custom implementation
}
}
// Now add the no_debug flag:
#[tagged_dispatch(MyTrait, no_debug)]
enum MyEnum { /* ... */ }The automatically generated traits are:
Debug- Shows enum and variant namePartialEq/Eq- Pointer equality (same object)PartialOrd/Ord- Orders by variant type, then pointer
If the automatic implementations work for your use case, simply remove your custom implementations.
§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.
Structs§
- Arena
Stats - Statistics for arena memory usage.
- BoxAllocator
- A simple box allocator for owned tagged pointers.
- Tagged
Ptr - The core tagged pointer type used internally.
Traits§
- Arena
Builder - Trait for arena builders generated by the macro.
- Tagged
Allocator - Allocator trait for arena-allocated tagged pointers.
Attribute Macros§
- tagged_
dispatch - Attribute macro for traits and enums to enable tagged pointer dispatch.