4

To learn the Rust language, I'm taking an old C++ library I had lying around and trying to convert it to Rust. It used a lot of C++11 closures and I'm having some difficulty getting the concepts to translate.

In C++ I had something like this:

// library.h
struct Event {
    // just some data
};

class Object {
public:
    // ...
    std::function<void(Event&)>& makeFunc(std::string& s) {
        return m_funcs[s];
    }
    // ...
private:
    // ...
    std::map<std::string, std::function<void(Event&)>> m_funcs;
    // ...
};

// main.cpp using the library
int main()
{
    Object foo;
    foo.makeFunc("func1") = [&]{
        // do stuff
    };
    return 0;
}

The part that I'm having trouble with is properly storing the functions in a Rust HashMap collection. I tried this:

struct Event;

struct Object {
    m_funcs : HashMap<String, FnMut(&Event)>
}

impl Object {
    // send f as another parameter rather than try and return borrow
    // compiler was complaining
    fn makeFunc(&mut self, s : &str,f: FnMut(&Event)) {
        self.m_funcs.insert(String::from_str(s), f); 
    }
}

but it says the trait core::marker::Sized is not implemented for the type 'for('r) core::ops::FnMut(&'r CreateEvent)'

This makes sense because FnMut is a trait, and therefore has no known size for the HashMap to make at compile time. So I figure that the hashmap would require an actual pointer rather than an abstract type. So I change it to this

struct Object {
    m_funcs : HashMap<String, Box<FnMut(&Event)>>
}

impl Object {
    fn makeFunc(&mut self, s : &str, f: &FnMut(&Event)) {
        self.m_funcs.insert(String::from_str(s), Box::new(f));
    }
}

now it says the trait 'for('r) core::ops::Fn<(&'r CreateEvent,)>' is not implemented for the type '&for('r) core::ops::FnMut(&'r CreateEvent)' [E0277] at the insert. This error makes no sense to me at all. Can someone explain to me the proper way to store a reference to a non-escaping closure in a HashMap?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
FatalCatharsis
  • 3,407
  • 5
  • 44
  • 74

1 Answers1

8

You have taken a &FnMut(&Event)—a trait object—and, after boxing it, wish to store it as a Box<FnMut(&Event)>. Thus, you require that &FnMut(&Event) must implement FnMut(&Event), which it does not (and clearly cannot, for FnMut.call_mut takes &mut self).

What you wanted was to take an arbitrary type that implements FnMut(&Event)—that is, use generics—and take it by value. The signature is thus this:

fn make_func<F: FnMut(&Event)>(&mut self, s: &str, f: F)

It gets a little more complex than this due to lifetimes, however, but what you wish to do with regards to that may vary; Storing an unboxed closure with a reference arg in a HashMap has more information on that topic. Here’s what I believe you’re most likely to want:

struct Object<'a> {
    m_funcs: HashMap<String, Box<FnMut(&Event) + 'a>>,
}

impl<'a> Object<'a> {
    fn make_func<F: FnMut(&Event) + 'a>(&mut self, s: &str, f: F) {
        self.m_funcs.insert(String::from_str(s), Box::new(f));
    }
}

You could remove all the 'a in favour of just a single + 'static bound on F if you are happy to not let any of the closures capture references to their environments.

Community
  • 1
  • 1
Chris Morgan
  • 86,207
  • 24
  • 208
  • 215
  • 1
    Now that i've read about it, I understand that this is the correct answer, but it has spawned another question for me here. http://stackoverflow.com/questions/29202698/how-to-use-a-trait-object-to-refer-to-struct-that-has-generic-methods – FatalCatharsis Mar 23 '15 at 03:07
  • What does the `+` operator do with lifetimes ? – crackpotHouseplant Nov 10 '19 at 00:42
  • Found it [this related thing](https://stackoverflow.com/questions/42028470/why-is-adding-a-lifetime-to-a-trait-with-the-plus-operator-iteratoritem-foo) during my journey – crackpotHouseplant Nov 10 '19 at 02:40