How to work closures?

Hello everyone

I’m learning about closures; I’ve been reading the documentation and I’m still unsure about when and where it’s recommended to use this type of function.
Another point of concern is that, whilst it allows you to use multiple immutable variables, it doesn’t use up any more memory.

Thank you very much for the explanation.

Closures are similar to JavaScript's arrow functions, for high level understanding, they can be aliased to anonymous functions. Perhaps mentioning JavaScript arrow functions in the closure documentation will help prevent this confusion from recurring in the future

While you can use normal functions, avoid closures, as they are difficult to debug in a debugger. Unless you're using println debugging

Fn = can only read the captured data, not write them
FnMut = can write to the captured variable
FnOnce = to limit it to only being called once, can write, and take ownership of the captured variable

Closures are not function pointers. The compiler will generate structs and impl block, for visualisation, let's say capturing a number, the compiler will generate

struct Add {
    num: i32,
}

impl Fn(i32) -> i32 for Add {
    fn call(&self, x: i32) -> i32 {
        x + self.num
    }
}

Therefore, the memory size of a closure depends on the size of the captured data type. For visulatisation :

fn main() {
    let a: i128 = 10;

    let fn_ptr: fn(i128) -> i128 = |x| x + 1;
    let no_capture = |x: i128| x + 1;
    let capture = move |x: i128| x + a;

    println!("fn pointer : {} bytes", std::mem::size_of_val(&fn_ptr));
    println!("no capture : {} bytes", std::mem::size_of_val(&no_capture));
    println!("move capture i128 : {} bytes", std::mem::size_of_val(&capture));
}

Output :

fn pointer : 8 bytes
no capture : 0 bytes
move capture i128 : 16 bytes

The size of i128 is 16 bytes, so the size of clossure becomes 16 bytes if it captures i128

In release mode, just like normal function, it will be inlined if the compiler thinks it is good to inline, so there is 0 memory cost

I forgot to answer the multiple mutable part. The rule of references is the fundamental of safe rust, so any other safe code that uses the normal references has to follow this rule. So if you capture normal references, you still can't have multiple mutable references to the same memory address at the same time. To be able to do it, you have to turn off the reference rules by using raw pointer or UnsafeCell

Note for further info in case you meet this case in the future:

  • RefCell doesn't allow you to do double mut at the same time, it just moves the compile time check into runtime panic
  • Arc Mutex doesn't allow too, because locking is equivalent to them not happening at the same time, and it is for multi threading

You usually write a closure when you encounter a function or method that takes a closure as an argument, like Iterator::map. That is, something that takes a generic with a Fn or FnMut or FnOnce bound (or the AsyncFn versions).

fn map<B, F>(self, f: F) -> Map<Self, F> ⓘ
where
    Self: Sized,
    F: FnMut(Self::Item) -> B,
let mut iter = a.iter().map(|x| 2 * x);

You usually write code that accepts closures yourself when you need to perform some arbitrary action which the caller supplies.

When you're storing some sort of completely arbitrary callback, you usually use Box<dyn Fn(..)> or dyn FnMut(..) or the like. Alternatively you could use function pointers, which would rule out closures which capture. But Box doesn't allocate for zero-sized types, so when the callback has no state, so there's less difference between Box<dyn Fn(..)> and fn(..) than you may have thought.

Huh, didn't know that. So returning a Box<dyn Fn(...)> from a function without capturing any state within means returning a ZST that won't even allocate anything at all? Pretty neat, actually.

No, although there's still one more indirection and the size is 2*usize versus 1 usize.

fn box_giver() -> Box<dyn Fn(usize, usize) -> usize> {
    Box::new(|x: usize, y: usize| x + y)
}

fn main() {
    let size = size_of_val(&box_giver());
    println!("{}", size); // prints 16...
}

something like this?
also, this might be a dumb question but is there a way to know if some allocation actually happened? like anyway to track if the box actually allocated? :sweat_smile:

As long as what @quinedot was saying as true - and I have no reasons to doubt his knowledge on the matter - you can figure out whether or not any allocation is about to take place beforehand, and enforce/branch things accordingly, even at compile time:

fn no_alloc<F>(f: F) -> Box<dyn Fn()>
where
    F: Fn() + 'static,
{
    const {
        if size_of::<F>() > 0 {
            panic!("only non-capturing `Fn()` are allowed")
        }
    };
    Box::new(f)
}

fn main() {
    // good
    let fn_box = no_alloc(|| println!("no capture"));
    // bad: won't compile
    let capture = String::from("text");
    let fn_box_cte = no_alloc(move || println!("captured: {capture}"));
}

indeed, they are the best!

could you please explain why size_of is 0 and size_of_val is 16 for the first call to no_alloc?
also, why did you place this inside a const block?
thanks a lot!

See if you can follow my train of thought here:

fn main() {
    // non-capturing closure doesn't capture at all
    // (as per the ... duh?), which gives the compiler
    // a chance to treat it as a ZST-like `fn()` which,
    // conventionally, are never placed/allocated
    // either on the stack or the heap at all:
    let fn_closure = || println!("no capture");
    // the `size_of` of it is trivially zero:
    assert_eq!(size_of_val(&fn_closure), 0);
    // and our `no_alloc` never fails
    let fn_box = no_alloc(fn_closure);

    // now compare it to this one,
    // where we are capturing by `move`:
    let capture = String::from("text");
    // by the point the compiler finishes constructing
    // our closure, it will have created (conceptually)
    // some tuple/struct of the form `(String, fn(&String))`,
    // where the `fn(&String)` alone can *still* be treated as a ZST,
    // but the `String` itself *must* be "captured" for the closure to work
    let fn_capture = move || println!("captured: {capture}");
    // the size of it must be equal to the size of the state being captured:
    assert_eq!(size_of_val(&fn_capture), size_of::<String>());
    // and our `const` + `panic!` check doesn't even let
    // our program compile if we wanted it to:
    let fn_box_cte = no_alloc(fn_capture);
}

I'm assuming you mean the size_of_val(fn_box)? As the size_of_val(f) is either:

// for the first closure
size_of_val(&f) = 0
// for the second one
size_of_val(&f) = 24

The fn_box is 16 bytes large (and 8 bytes aligned) because the underlying pointer is. Box<T> is just a wrapper around a Unique<T>, which is itself a wrapper around NonNull<T>, which is - yes, you've guessed it - another wrapper yet, around a plain raw pointer to *const T this time:

// "go to definition" on any `Box` in your IDE:
pub struct Box<T: ?Sized, A: Allocator = Global>(Unique<T>, A);
// go to `Unique<T>`:
pub struct Unique<T: PointeeSized> {
    pointer: NonNull<T>,
    _marker: PhantomData<T>,
}
// go to `NonNull<T>`:
pub struct NonNull<T: PointeeSized> {
    pointer: *const T,
}

Therefore, since our fn_box is indeed a Box<dyn Fn()>, a size_of_val(&fn_box) is, in fact, same as size_of::<*const dyn Fn()>; and the reason the a *const dyn Fn() is 16 bytes large, instead of the usual 8 bytes for a regular pointer[1], is because of what's required to refer to any trait object out there: two pointers, meshed together, passed around as one (a "fat" pointer).

The first references the underlying state (the captured String), the other one - the function (the fn(&String) in this case) being called. Except that this last part is not quite right, either: you can only "get" to that function by looking it up in the "virtual table" first. It references all of the available methods for that particular trait object type, and it's this vtable reference that is actually being stored in the slot of the second pointer for the *const dyn Fn(), wrapped 3 times over into a Box.

It's just another way for enforcing certain conditions at compile-time.

fn no_alloc<T>(f: T) -> Box<T>
{
    // this is not allowed, and will blow up with:
    // `error[E0401]: can't use generic parameters from outer item`
    const VALIDATE: () = {
        if size_of::<T>() > 0 {
            panic!("only non-capturing `Fn()` are allowed")
        }
    };
    // this is allowed, and will only run *if* the `no_alloc` itself
    // is ever referenced/called, at any point, in your actual source code
    const {
        if size_of::<T>() > 0 {
            panic!("only non-capturing `Fn()` are allowed")
        }
    };
    Box::new(f)
}

  1. on a typical 64-bit OS ↩︎