Idiomatic rust way to generate unique id

Consider this code which guarantees (up to i64 overflow) unique ids:

pub struct IDManager {
  next_id: Cell<i64>
}

impl IDManager {
  pub fn new() -> IDManager {
    IDManager { next_id: Cell::new(1) }
  }

  pub fn get_id(&self) -> i64 {
    let ans = self.next_id.get();
    self.next_id.set(ans + 1);
    ans
  }
}

Now, I want a singleton IDManager which is to be used in lots of functions.

Is the idiomatic Rust way to do this:

  1. create a IDManager in main, and pass it around

  2. create a global via lazy_static

I'm using this in rust/wasm32, so concurrency is not a issue.

I'd use a global atomic integer and not use lazy_static. You don't need the atomicity in wasm, but it doesn't hurt you either, and avoiding lazy_static gives you some (insignificant) performance back. Plus for free your code works with threads.

See e.g. the thread counting example in https://doc.rust-lang.org/std/sync/atomic/

3 Likes

Specifically:

static COUNTER = AtomicUsize::new(1);
fn get_id() -> usize { COUNTER.fetch_add(1, Ordering::Relaxed) }

This is the exact kind of things atomics are for.

3 Likes

@droundy : On a related note, I was not aware of the overhead of lazy_static. Is there any overhead besides the initialization ?

@CAD97: Thanks for sample code -- I'm (pleasantly) surprised by the simplicity of the solution.

Another nice touch: you can put the static inside the function to make sure nobody can mess with it:

use std::sync::atomic::{AtomicUsize, Ordering};
fn get_id() -> usize {
    static COUNTER:AtomicUsize = AtomicUsize::new(1);
    COUNTER.fetch_add(1, Ordering::Relaxed)
}
7 Likes

At least an atomic load on every access, so nothing to worry about in the vast majority of cases (esp. with a branch predictor)

1 Like

@scottmcm :

static inside function

is really cool -- and for whatever reason, I did not realize this was possible until now

Future tip: Rust is a very uniform language. Wherever you can put an item, there's a good chance you can put any other item there, too. You can put functions inside functions, modules inside functions, heck, even modules inside a block that is the initializer expression of a static – playground.

This is not a coincidence: it makes writing code much easier, implementation details more elegant to hide, and a lot of useful macro-based tricks possible at all.

2 Likes

It requires checking on every dereference whether it had been initialized. It's essentially like the C pattern of setting a pointer to null and then on every use checking if it's null and initialized it if that is the case.

https://docs.rs/lazy_static/1.4.0/lazy_static/#implementation-details

1 Like

does this move the initialization to first-call time, as this would do in C++? (i'd venture guess not, because if so it's essentially the same as lazy_static)

A static has no initialization code to run. This is why you can only assign it constants or outputs from a const fn, because otherwise it is not able to hard code the initial value in the binary.

Rust does typically not have code that runs before main.

5 Likes

No. In rust, where you put an item (fn, struct, impl, static, ...) never affects what it does only where it's visible.

1 Like