You can use a Zero Sized Type (ZST, for short) to get the API you want, without the overhead of another pointer.
Here is an implementation for 2 pools, which can be extended to support any number of pools using a macro to generate the "marker" struct (P1
, P2
, etc).
A major downside is that forgetting to free
using the pool will "leak" the memory.
This Ferrous Systems blog post has a number of possible improvements that might interest you, especially if you statically allocate the pools, and they also have a number of tricks for playing with the visibility of P1
so that users wont be able to misuse the API.
use std::marker::PhantomData;
use std::{cell::RefCell, mem::size_of};
struct Index<D>(usize, PhantomData<D>);
struct Pool<D> {
data: Vec<[u8; 4]>,
free_list: RefCell<Vec<bool>>,
marker: PhantomData<D>,
}
impl<D> Pool<D> {
fn new() -> Pool<D> {
Pool {
data: vec![[0,0,0,0]],
free_list: vec![true].into(),
marker: PhantomData::default(),
}
}
fn allocate(&self) -> Index<D> {
self.free_list.borrow_mut()[0] = false;
Index(0, self.marker)
}
fn free<'a>(&self, item: Index<D>) {
self.free_list.borrow_mut()[item.0] = true;
}
}
struct P1;
fn create_pool1() -> Pool<P1> {
assert_eq!(size_of::<Index<P1>>(), size_of::<usize>());
Pool::new()
}
struct P2;
fn create_pool2() -> Pool<P2> {
Pool::new()
}
fn main() {
let global_pool1 = create_pool1();
let global_pool2 = create_pool2();
let new_obj1 = global_pool1.allocate();
let new_obj2 = global_pool2.allocate();
global_pool1.free(new_obj1);
global_pool2.free(new_obj2);
global_pool1.free(new_obj2);
global_pool2.free(new_obj1);
}
Trying to free using the wrong pool results in:
error[E0308]: mismatched types
--> zpool\src\main.rs:57:23
|
57 | global_pool1.free(new_obj2);
| ^^^^^^^^ expected struct `P1`, found struct `P2`
|
= note: expected struct `Index<P1>`
found struct `Index<P2>`
Link to playground
This can be improved a little so that the borrow checker will enforce that Index
will not out-live the Pool
, using:
fn allocate(&self) -> Index<&'_ D> {
self.free_list.borrow_mut()[0] = false;
Index(0, Default::default())
}
So you get this error if the pool is dropped while an Index
is alive:
error[E0505]: cannot move out of `global_pool1` because it is borrowed
--> zpool\src\main.rs:54:10
|
49 | let new_obj1 = global_pool1.allocate();
| ------------ borrow of `global_pool1` occurs here
...
54 | drop(global_pool1);
| ^^^^^^^^^^^^ move out of `global_pool1` occurs here
...
58 | println!("{}", new_obj1.0);
| ---------- borrow later used here
Link to playground
Also, a link to playground with Item
API (returning an Item
, vs only and Index
)
unsafe
code, the compiler will guarantee that everything is dropped once at the end of its respective lifetime without any exceptions. I imagine anything else would likely be considered undefined behavior. If it can't make that guarantee, then it will refuse to compile your code. This is one of the big selling points of rust and is so essential to the rust language that you can always assume it to be true in future updates. – Locke