Referencing functions inside impls that have a lifetime


#1

Hi,

I’m relatively new to Rust and loving it. There’s one thing I haven’t got my head around yet, and this code demonstrates it:

struct Data<'a> {
    x: &'a String
}

impl<'a> Data<'a> {
    fn printer(&self) {
        println!("{}", self.x);
    }
}

fn main() {
    let s = "My String".to_string();
    let data = Data { x: &s };

    let handler: fn(&Data) = Data::printer;

    handler(&data);
}

Compiling this gets the error “expected concrete lifetime, found bound lifetime parameter”. I’ve Googled this error and haven’t found a scenario similar enough so I can make sense of what it’s saying.

I know if I get rid of the 'a lifetime on Data, everything works fine. If I make the printer function take a &Data as its argument instead of using &self, it also works fine.

My ultimate use of this construct is to have a const array that defines a bunch of method pointers for a parser. Any help would be greatly appreciated!


#2

Note that this works fine if you drop the fn(&Data) bit and let type inference do its thing.

(playground link)

The technical reason for why you’re getting this error (or my understanding of it anyway) is that 'a in the Data impl is meant to be linked to the concrete lifetime of some actual object, but when you reference it using the type (Data::printer), it doesn’t know what object to bind the lifetime to.

When I’m designing an API I generally try to either be generic over any type of callable so people can use closures, you can’t store an array of closures though because each closure is its own type and arrays can only contain homogeneous types.

It sounds like you may have an X-Y problem. I’ve done a lot of parser work and even written several toy languages and parser/serialization libraries for custom file formats at work and never found a situation where I’m using an array of function pointers. That doesn’t actually mean much though because we’re probably doing things in completely different ways :stuck_out_tongue_winking_eye:

If you can explain a bit more about how you’re trying to approach things maybe I (or someone else) may see an easy way to express what you’re trying to do.

A small nit: you’re probably also wanting to use a &'a str as the type of x instead. A String is a string of characters explicitly allocated on the heap, whereas a &str is a slice into any string (whether it’s on the heap, compiled into the executable, or sitting on the stack somewhere). It’s a similar idea to the difference between a Vec (or std::vector if you’re familiar with C++) and an array. It’s a bit of a learning curve, but knowing the difference between a heap allocated string and a string slice can often make a decent performance difference when you’re writing parsers because of all the small strings you’re pushing around.

EDIT: I just tried using mem::transmute to force the compiler to interpret Data::printer as a function pointer directly and it looks like Data::printer is actually zero-sized. So that means turning it into a function pointer probably doesn’t make sense because there’s nothing to actually point to ¯_(ツ)_/¯


#3

The workaround is;

    fn data_printer(d: &Data) { d.printer(); }
    let handler: fn(&Data) = data_printer;

I can tell you every function is its own unique type. You then can upcast to a more general type. Rust does not allow the lifetime to be removed so easily. I know from what I’ve read multiple copies of a function with lifetime can be created by the compiler when called in different parts of the code. Would need someone with more knowledge to say why the upcast to concrete lifetime isn’t allowed.

Alternative is to use a lifetime but has downside;

  • one passed into a function
  • fn(&Data<'static>)
  • struct Ha<'a>(fn(&Data<'a>));

#4

Thanks. I think I simplified my example too much. My real use case looks like this:

struct CommandSettings {
    name: &'static str,
    argument_count: i32,
    handler: fn(&Command) -> CommandResult
}

const COMMAND_SETTINGS: [CommandSettings; 14] = [
    CommandSettings { name: "LLEN", argument_count: 1, handler: Command::llen },
    CommandSettings { name: "LPOP", argument_count: 1, handler: Command::lpop },
    ...etc...

@jonh, looks like I’ll have to go down your suggested path. I was just hoping there was some nice way to do it :slight_smile:


#5

I think I figured out how to do it in a way that’s not too ugly.

const Commands: [&Fn(&Data); 1] = [
    &|data| { println!("len: {}", data.len()); },
];

Basically you’re creating a static array of pointers to some callable which takes a reference to your Data. This actually uses your closure as a trait object (which is why I had to put & in front), but that’s effectively what you were wanting to do, isn’t it?

So you’d probably do something like this…

struct CommandSettings {
    name: &'static str,
    argument_count: i32,
    handler: &Fn(&Command) -> CommandResult
}

const COMMAND_SETTINGS: [CommandSettings; 14] = [
    CommandSettings { name: "LLEN", argument_count: 1, handler: &|cmd| { cmd.len() } },
    CommandSettings { name: "LPOP", argument_count: 1, handler: &|cmd| { cmd.lpop() } },
    ...
];

#6

In Rust 1.19 (released yesterday), you can do:

struct Data<'a> {
    x: &'a String
}

impl<'a> Data<'a> {
    fn printer(&self) {
        println!("{}", self.x);
    }
}

fn main() {
    let s = "My String".to_string();
    let data = Data { x: &s };

    let handler: fn(&Data) = |d| Data::printer(d);

    handler(&data);
}

#7

Building on what @jethrogb mentioned, you can simplify this a bit to:

struct CommandSettings {
    name: &'static str,
    argument_count: i32,
    handler: fn(&Command) -> CommandResult
}

const COMMAND_SETTINGS: [CommandSettings; 14] = [
    CommandSettings { name: "LLEN", argument_count: 1, handler: |cmd| { cmd.len() } },
    CommandSettings { name: "LPOP", argument_count: 1, handler: |cmd| { cmd.lpop() } },
    ...
];

The specific feature in 1.19 that enables this is https://github.com/rust-lang/rust/pull/42162.


#8

That’s essentially what that 1.19 feature (non capturing closure coercion to fn) does. It doesn’t add a “dummy” fn but rather encodes the dispatch into the (anonymous) closure type.

What do you mean? Lifetime params are generic, but compiler doesn’t create multiple copies of the function for each concrete lifetime (like it would with generic type params - i.e. monomorphization). Lifetimes, unlike types, aren’t present in generated machine code, and are a type system only concept.