templatesrustclosures

closure template parameter with same prototype but different body, count as different types for monomorphization by the rust compiler?


I'm coming from a C++ background. A C++ template is compiled for each unique instance of the template's type(s).

template <typename T>
T add(T a, T b) {
    return a + b;
}

...
int intResult = add(3, 5);           // the function was compiled for int specialization
double doubleResult = add(3.5, 2.1); // the function was compiled for double specialization

On to rust. For something like this (a function which takes a closure as an argument):

pub fn copy_checked<R1, R2>(&'sdl mut self, path: &str, src: R1, dst: R2) -> Result<(), String>
    where
        R1: Into<Option<Rect>>,
        R2: Into<Option<Rect>>,
    {
        RenderSystem::get_or_load_and_use( // closure arg here
            |txt: &Texture<'_>| self.cc.canvas.copy(&txt, src, dst),
        )
    }

The above fails because R1 and R2 also require the copy trait.

cannot move out of `dst`, a captured variable in an `FnMut` closure
move occurs because `dst` has type `R2`, which does not implement the `Copy` trait

This has made me uncertain on how exactly rust templates are implemented. If it was like C++, then there should be no need to copy the args. The function would just be inlined compiled in. But if it's doing a copy, that means that different lambdas with the same signature (args and return type) wouldn't need to have different compiled instances. They could have the same function body, but only a different function pointer is used.

In summary: Do closure template parameters with the same prototype (args and return type) but different body count as different types for monomorphization by the rust compiler?


Solution

  • Your question touches on a number of interrelated things:

    1. Each closure definition creates a unique anonymous type even with the same signature.

    2. The Rust compiler does use monomorphization to compile generics - meaning calling a function with different closures will compile separate functions for each unique closure that is passed.

    3. Rust does not wait until monomorphization to return errors. In fact a goal is that there are no errors at monomorphization time. Instead all constraints are checked up front which must hold for all cases. This is the biggest different between C++ templates and Rust generics.

    So even if you pass types for R1 and R2 that do implement Copy and would theoretically pass monomorphization, you don't constrain the generic types R1 and R2 to be Copy and thus the function body doesn't pass the constraint solver.


    The Copy constraint is not due to monomorphization, but due to ownership. You're passing src and dst into .copy by value meaning you lose ownership after passing them. But the error indicates the closure passed to get_or_load_and_use is FnMut - which can be called multiple times and thus .copy may be called multiple times. The only way you can pass ownership of something multiple times is if it implements Copy. So for this to compile, you need to add a constraint that R1 and R2 are Copy:

    where
        R1: Into<Option<Rect>> + Copy,
        R2: Into<Option<Rect>> + Copy,
    

    Or if Copy is too restrictive. You can instead constrain on Clone and pass clones to .copy:

    self.cc.canvas.copy(&txt, src.clone(), dst.clone())
    

    See What is the difference between Copy and Clone?