| Crates.io | tagged_dispatch |
| lib.rs | tagged_dispatch |
| version | 0.3.0 |
| created_at | 2025-09-16 13:59:13.117952+00 |
| updated_at | 2025-09-20 13:35:12.954834+00 |
| description | Memory efficient trait dispatch using tagged pointers. |
| homepage | |
| repository | https://github.com/khalen/tagged_dispatch |
| max_upload_size | |
| id | 1841751 |
| size | 93,008 |
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.
no_std (bring your own allocator)Add this to your Cargo.toml:
[dependencies]
tagged_dispatch = "0.3"
# Optional: Enable arena allocation support
tagged_dispatch = { version = "0.3", features = ["allocator-bumpalo"] }
std (default): Standard library supportallocator-bumpalo: Implements TaggedAllocator for bumpalo::Bumpallocator-typed-arena: Implements TaggedAllocator for typed_arena::Arena<T>all-allocators: Enables all allocator implementationsuse 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);
tagged_dispatch when:enum_dispatch when:Without lifetime parameters on the enum, generates owned tagged pointers using Box:
Box::into_raw(Box::new(value))Drop to deallocateClone that deep-copiesWith lifetime parameters on the enum, generates arena-allocated pointers:
TaggedAllocator traitCopy (just copies the 8-byte pointer)Send, Sync, or even SizedFor 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);
}
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)");
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
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 implementations
Note that all comparison traits use pointer equality, not value equality. Two instances are equal only if they point to the same object.
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");
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 pointerIf the automatic implementations work for your use case, simply remove your custom implementations.
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).
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.
This crate uses unsafe code for tagged pointer manipulation. I've tried to carefully document and test all unsafe operations.
TaggedPtr are valid, properly aligned, and point to initialized dataDrop implementation (in the default boxed implementation) ensures no memory leaksThe crate contains the following unsafe operations:
Pointer Dereferencing (TaggedPtr::as_ref, TaggedPtr::as_mut):
Memory Deallocation (in generated Drop impl):
untagged_ptr() to ensure the original pointer is passed to Box::from_rawType Transmutation (in generated code):
Send/Sync Implementation:
TaggedPtr<T> is Send/Sync if and only if T is Send/SyncAll unsafe code is contained within the library implementation and is not exposed to users.
Licensed under either of
at your option.
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.