Is it possible to have a generic readonly Arc singleton that I can clone?

I'm not sure my title is using all the correct terms I think this broken code gets across my goal:

fn new_empty<T>() -> Arc<T> where T: Default {
    const EMPTY: Arc<T> = Arc::new(T::Default());
    EMPTY.clone()
}

I have a Arc<T> where T is a buffer that requires alloc.

I sometimes want an Arc<T> where the buffer is empty. I currently do that with this code: Arc::new(T::Default()).

But that code allocs a new buffer each time. Instead I would like to have a singleton Arc<T> with an empty buffer that I can just clone.

The solution doesn't need to use const, doesn't need to be a function... but it does need to be generic and allow me to clone an existing Arc<T> instead of alloc each time.

Is this possible?

Thanks,
Jesse

Sure, use lazy_static instead of an ordinary constant.

lazy_static! {
    static ref EMPTY: Arc<T> = Arc::new(T::default());
}

Thanks, but where do I put that?

If I try:

fn new_empty<T>() -> Arc<T> where T: Default {
    lazy_static! {
        static ref EMPTY: Arc<T> = Arc::new(T::default());
    }
    EMPTY.clone()
}

Then I get error:

can't use generic parameters from outer function
use of generic parameter from outer function rustc(E0401)

Previously I tried to work through the workaround suggestions in rustc(E0401), but got lost in the process.

Ah.. I don't know if it's possible with those generics. Try with OnceCell instead.

You can't make this generic, because Rust doesn't currently support generic static items

You can use macros to reduce the boilerplate.

You can get something similar to this with a manual HashMap<TypeId, ...>, as long as T: 'static:

use once_cell::sync::Lazy;
use std::{
    any::{Any, TypeId},
    collections::{hash_map::Entry::*, HashMap},
    sync::{Arc, RwLock},
};

fn new_empty<T>() -> Arc<T>
where
    T: 'static + Send + Sync + Default,
{
    static EMPTIES: Lazy<RwLock<HashMap<TypeId, Box<dyn Any + Send + Sync>>>> = Lazy::new(Default::default);
    let e = EMPTIES
        .read()
        .unwrap()
        .get(&TypeId::of::<T>())
        .map(|arc| arc.downcast_ref::<Arc<T>>().unwrap().clone());
    e.unwrap_or_else(|| match EMPTIES.write().unwrap().entry(TypeId::of::<T>()) {
        Occupied(o) => o.get().downcast_ref::<Arc<T>>().unwrap().clone(),
        Vacant(v) => {
            let i = <Box<Arc<T>>>::default();
            let r = Arc::clone(&i);
            v.insert(i);
            r
        }
    })
}

fn main() {
    let a: Arc<i32> = new_empty();
    let b: Arc<u8> = new_empty();
    let c: Arc<()> = new_empty();
    let d: Arc<i32> = new_empty();
    let e: Arc<i32> = new_empty();
    println!("{0:?} {0:p}\n{1:?} {1:p}\n{2:?} {2:p}\n{3:?} {3:p}\n{4:?} {4:p}", a, b, c, d, e);
}

(playground)

Edit: One step of indirection and allocation can be avoided by using SmallBox (click here for an example)
use default::default;
use once_cell::sync::Lazy;
use smallbox::{smallbox, space::S2, SmallBox};
use std::{
    any::{Any, TypeId},
    collections::HashMap,
    sync::{Arc, RwLock},
};

fn new_empty<T>() -> Arc<T>
where
    T: 'static + Send + Sync + Default,
{
    static EMPTIES: Lazy<RwLock<HashMap<TypeId, SmallBox<dyn Any + Send + Sync, S2>>>> =
        Lazy::new(default);
    let cast =
        |r: &SmallBox<dyn Any + Send + Sync, S2>| r.downcast_ref::<Arc<T>>().unwrap().clone();
    let option = EMPTIES.read().unwrap().get(&TypeId::of::<T>()).map(cast);
    option.unwrap_or_else(|| {
        cast(
            EMPTIES
                .write()
                .unwrap()
                .entry(TypeId::of::<T>())
                .or_insert(smallbox!(Arc::<T>::default())),
        )
    })
}

Edit2: Make sure not to miss this:

3 Likes

for the macro solution I was referring to earlier, https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=395e98b58cb8b688c4ed37feed96215b

2 Likes

Would std::mem::MaybeUninit work? doc

Where would you use that? The problem is that you need the constant to be duplicated for each generic parameter.

1 Like

Oh. I guess I was picturing the new function associated with a struct and somehow not dealing with a const.

Would that then allow an associated item to be used instead of the const?

Oh wow... that's a tricky one. Macros all the way!

More of a theoretical discussion than anything else, but I feel like Send may not be needed for new_empty() (using unsafe to skip the bound, I mean): the static map and thus its values will never be dropped, so there should be no way for an exclusive access to the dyn Any + Sync to cross thread boundaries :thinking:

Obviously in practice it doesn't matter, since I'd have yet to see somebody being hindered by their 'static + Sync + Default + !Send type not being usable :grinning_face_with_smiling_eyes:


Also, one step of indirection may be always avoided if using Arc<dyn Any + …> (Playground).

2 Likes

I had the same idea while writing the Send bound, but I skipped that because my main goal was to use no unsafe.

dammit, I thought about it but forgot about Arc::downcast being a thing!

1 Like