Helper for most common use of `std::sync::Once`

Probably the most common thing std::sync::Once is used for is to ensure safe lazy initialization of a global, like this:

use std::sync::{Once, ONCE_INIT};
fn expensive_integer() -> i32 {
    static mut EXPENSIVE_INTEGER: i32 = 0;
    static mut EXPENSIVE_INTEGER_INIT: Once = ONCE_INIT;
    EXPENSIVE_INTEGER_INIT.call_once(|| {
        unsafe { EXPENSIVE_INTEGER = /* expensive computation */; }
    });
    unsafe { EXPENSIVE_INTEGER }
}

This is repetitive and requires unsafe blocks. I’d like to suggest a wrapper that can be used like this instead

use std::sync::InitOnce;
static expensive_integer: InitOnce<i32> = InitOnce::new(|| /* expensive computation */);

Under the hood it’s doing the exact same thing, and then implementing Deref so that it looks like a normal immutable value of type T to consumers.

In addition to being shorter and not requiring application code to use unsafe, please note that in my first example, all of the expensive computation is inside an unsafe block, unless the author takes special care to hoist it. In the second example, that naturally doesn’t happen.

What do people think? This seems simple enough that I may just implement it and send a PR unless everyone hates it.

(Yes, this is essentially what lazy_static does with different syntax. You can’t always use third party crates.)

3 Likes

Sounds nice. What is the rationale for this design compared to just having lazy_static in stdlib?

Frankly I hadn’t thought at all about that possibility. I’m not the author of lazy_static and I have no idea what they think about merging or not merging it into the stdlib. This design doesn’t involve any macros, but it might turn out to be unusably clunky in some respects if it stays that way (for instance it might be necessary to write static expensive_integer: InitOnce<i32> = InitOnce::<i32>::new(...) which is getting awfully repetitive…)

In summary: :man_shrugging:

Pinging @Kimundi on lazy_static.

Type inference should be able to elide ::<i32> bit at least so you would have:

static expensive_integer: InitOnce<i32> = InitOnce::new(...);
// and for length-visual comparison:
static expensive_integer: InitOnce<i32> = InitOnce::<i32>::new(...)

This gets more important the longer the type becomes of course.

I wonder if this could be further alleviated by allowing you to elide the type entirely on static (and perhaps const):

static expensive_integer = InitOnce::<i32>::new(...)

At that point I think it’s quite ergonomic. But let’s not block on a hypothetical =)

+100 to build-in lazy static without macros.

One thing that bothers me though is that ideally we want to parametrize InitOnceover the function as well, so as to avoid any possibility of indirect call :slight_smile: But I am 100% sure that this’ll be a premature optimization, and storing, as proposed, fn() -> T function pointer will be good enough, and quite probably LLVM will be able to optimize indirect call away anyway.

But looks like to implement InitOnce we’ll need const fn? But that’s probably OK, because calling const fn on stable should be available soon.

Will it be possible to use InitOnce for non-staic lazy variables? Like in struct S { data: InitOnce<i32> }?

Should we use the name Lazy for this type? Should we provide a lazy convenience function, so that the origical example looks like

static expensive_integer: Lazy<i32> = lazy(|| {
    // expensive computation
})
3 Likes

FWIW, .NET has a Lazy<T> class that’s very useful: https://docs.microsoft.com/en-us/dotnet/api/system.lazy-1

I love that in Rust we can do better than a LazyThreadSafetyMode parameter to the constructor :heart:

1 Like

I agree this would be great. In fact we already have something like this in libstd, just a bit specialized: io::lazy::Lazy. Would be nice to be able to reuse other code there :smiley:

EDIT: Actually all that really needs is a reentrant mutex with a const fn initialization function. That’s not easy to do on POSIX though…

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.