Sized compile error in when using "standard" function syntax but not when using where in higher order function?

Hi,
I was playing around a bit with higher order functions to try to get rid of a Box that I had been using before when I couldn't manage to get any alternative to compile. But now I managed to get a function compiled that takes another function without using any reference. I didn't think this was possible in Rust because I assumed it would always be unsized. I also thought that using where in a function was another syntax for the "standard" way of just putting the colon when the trait object is first introduced.

Anyway here is the example that complains that the function being received is unsized.

pub fn unboxed_higher_order<T: Debug + Serialize + Copy>(
    vector: &[u8],
    deserialize: FnOnce(&[u8]) -> Vec<SomeStruct<T>>,
) {
    let deserialized = deserialize(vector);
    println!("{:?}", deserialized);
}

And here is the version that is working:

pub fn unboxed_higher_order2<T, F>(vector: &[u8], deserialize: F)
where
    T: Debug + Serialize + Copy,
    F: FnOnce(&[u8]) -> Vec<SomeStruct<T>>,
{
    let deserialized = deserialize(vector);
    println!("{:?}", deserialized);
}

FnOnce(&[u8]) means dyn FnOnce(&[u8]) -> Vec<SomeStruct<T>> which is unsized. Change it to impl FnOnce(&[u8]) -> Vec<SomeStruct<T>>.

1 Like

Using a where clause is not the same, as you have observed. FnOnce is actually a trait (albeit one with super special syntax). The difference here is that in one case you're asking for the function to take what's known as a trait object — something that could be any value implementing the trait, and as such could have any size. In the other case, your function is generic.

The main thing to be aware of with generic functions is that the compiler will generate a copy of the function for every choice of F you call it with. This is why it knows the size — each copy of the function is specialized for a specific instance of the FnOnce trait, and it knows the size of that specific instance. The caller can pick which value of F to use, as long as it satisfies the trait constraints you have listed.

Note that every closure in your program has a unique type. Read more here.

Trait objects don't cause separate versions of the function to be compiled — they use dynamic dispatch at runtime instead. Since it could be any FnOnce, the compiler can't predict the size at compile time.

The impl FnOnce syntax suggested by @hashedone is just an alternate way to write the where bound.

3 Likes

I have read through the rust book and probably used both trait objects and generics many times already but I never realized there was a difference. Thanks for pointing that out! I will look more into the link and try to wrap my head around this.

Note that the <T> part in my_function<T>(t: T) is called a type parameter for a reason. The user can choose any type T to pass to the function, just like the user can pick what value to use as normal arguments. Type parameters are just chosen at compile time.

Where bounds are constraints on what types the caller can pick, just like types are constraints on what kind of value the caller can pick.

Yeah, I think I've had a pretty good grasp of generics (even though I didn't realize what they were called). My confusion probably stems from me thinking that the different syntaxes were just different ways to express generics or static dispatch. Didn't strike me before that dyn might have something to do with dynamic dispatch.

Followup question: If I were to use a trait object instead I know that I have to put it behind some kind of reference. But is there anyway to put it behind a simple reference & or does it always have to be a Box or some other smart pointer? If I try to use & it complains about trying to dereference the deserialize function that doesn't have a size known at compile time.
I'm guessing the answer to this is that it's not possible because I've never seen an example using a simple reference, and in that case, is it because the data of a Box lives on the heap and that the data of & can live either on the stack or the heap? Or is there some other explanation?

FnOnce consumes itself by value, this is currently incompatible with trait objects and Box<dyn FnOnce()> only recently became usable, don't try to learn anything about trait object from it as it is a special case.

Fn() and FnMut() take themselves by reference so they work fine as trait objects without any special casing.

1 Like

You can totally have references to trait objects:

fn print(obj: &dyn Display) {
    println!("{}", obj);
}

fn main() {
    print(&"test");
    print(&5);
}

playground

Just put dyn on the reference like you would put mut on a mutable reference, and you should be good.

The issue you ran into is likely that the function on the trait takes self by value, so to call it you need to know the size as that is required to pass something by value.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.