Help needed regarding the lifetime of Box<dyn FnOnce(...)>

I am trying to write flexible code in rust and have come to this problem just now. I want to have a function that applies some user-defined behavior to the data. But because the parameter f of function alter actually has a type of Box<(dyn FnOnce(...) + 'static)>, told by the compiler, I cannot directly use the reference num since it is not static. I then tried to copy it in inside the box to make it live as long as the box. But the error is still there. I am wondering why this is the case and is there any workaround to this?


trait Trait<N: Eq + Hash + Clone> {
    fn alter(hm: &mut HashMap<N, HashSet<N>>, k: &N, f: Box<dyn FnOnce(Option<&mut HashSet<N>>)>) {
        f(hm.get_mut(k))
    }

    fn delete(hm: &mut HashMap<N, HashSet<N>>, k: &N, num: &N) {
        Self::alter(
            hm,
            k,
            Box::new(|m_hs| {
                let cnum = num.clone();
                m_hs.map_or((), |hs| {
                    hs.remove(&cnum);
                })
            }),
        );
    }
}

i32 is both small and Copy, so there’s not much point in using &i32:

fn delete(hm: &mut HashMap<i32, HashSet<i32>>, k: &i32, num: i32) {
    alter(
        hm,
        &1,
        Box::new(move |m_hs| {
            m_hs.map_or((), |hs| {
                hs.remove(&num);
            })
        }),
    );
}

Alternatively, you can use impl FnOnce… instead of Box<dyn FnOnce…>:

fn alter(
    hm: &mut HashMap<i32, HashSet<i32>>,
    k: &i32,
    f: impl FnOnce(Option<&mut HashSet<i32>>),
) {
    f(hm.get_mut(k))
}
2 Likes

Thank you so much. I tried to catch the essence of the original problem, so I simplify the type a bit. It seems I have failed to do so. I have updated code now, it's closer to the original problem. Those functions were originally inside a trait and I want it to be object-safe so I used a Box there. The move keyword doesn't help now, what's the difference, why it worked previously but not now?

You need to do the clone outside the closure, and also ensure that N is itself ’static:

trait Trait<N: Eq + Hash + Clone + 'static> {
    fn alter(hm: &mut HashMap<N, HashSet<N>>, k: &N, f: Box<dyn FnOnce(Option<&mut HashSet<N>>)>) {
        f(hm.get_mut(k))
    }

    fn delete(hm: &mut HashMap<N, HashSet<N>>, k: &N, num: &N) {
        let cnum = num.clone();
        Self::alter(
            hm,
            k,
            Box::new(move |m_hs| {
                m_hs.map_or((), |hs| {
                    hs.remove(&cnum);
                })
            }),
        );
    }
}

Depending on your actual application, you may also be able to use Box<dyn ‘_ + FnOnce…> instead to remove the ’static requirement:

trait Trait<N: Eq + Hash + Clone> {
    fn alter(hm: &mut HashMap<N, HashSet<N>>, k: &N, f: Box<dyn '_ + FnOnce(Option<&mut HashSet<N>>)>) {
        f(hm.get_mut(k))
    }

    fn delete(hm: &mut HashMap<N, HashSet<N>>, k: &N, num: &N) {
        Self::alter(
            hm,
            k,
            Box::new(|m_hs| {
                m_hs.map_or((), |hs| {
                    hs.remove(num);
                })
            }),
        );
    }
}
2 Likes

Thank you again! The code type checks now! I certainly don't want the type parameter to be static as it will make a lot of other things static as well and make the code not so elegant or efficient I suppose. But I think the more interesting part now is why we have to manually set the lifetime and why move does not work this time, are there any readings or blogs that are relevant and you can point me to? Appreciate it.

dyn Trait in most places is the same as dyn Trait + 'static. Adding the lifetime removes the 'static. This section in the reference explains more.

When you have this:

let num = &N;
|| hs.remove(num);

You're capturing &&N. So adding move just changes it to &N which is still borrowing. You need to use clone outside the closure to do what you want.

let num = &N;
let num = num.clone();
move || hs.remove(num);

If you're worried about efficiency, I would change this to &mut dyn FnMut so you don't need to allocate the Box.[1]

pub trait Trait<N: Eq + Hash + Clone> {
    fn alter(
        hm: &mut HashMap<N, HashSet<N>>,
        k: &N,
        f: &mut (dyn FnMut(Option<&mut HashSet<N>>)),
    ) {
        f(hm.get_mut(k))
    }

    fn delete(hm: &mut HashMap<N, HashSet<N>>, k: &N, num: &N) {
        Self::alter(hm, k, &mut |m_hs| {
            m_hs.map(|hs| {
                hs.remove(num);
            });
        });
    }
}

This also removes the need for + '_ since the containing type &mut has a lifetime.


  1. If you have a true FnOnce closure, you can wrap it in Option and another closure to call it as FnMut: Rust Playground ↩︎

4 Likes

Here's my alternative page on (basic) dyn Trait lifetime elision (almost no-one needs the advanced version).

2 Likes

Very clear explanation, thank you so much! This is what I've been looking for.