最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

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

programmeradmin0浏览0评论

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.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?

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.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?

Share Improve this question edited Feb 1 at 21:07 cafce25 27.6k5 gold badges45 silver badges58 bronze badges asked Feb 1 at 20:49 jagprog5jagprog5 1311 silver badge6 bronze badges 3
  • 1 I fear you've gone down the wrong line of thinking - your error is completely unrelated to monomorphization. The error is 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 implies that it 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. – kmdreko Commented Feb 1 at 21:01
  • @kmdreko that make sense, thanks. However, my overall question still stands. – jagprog5 Commented Feb 1 at 21:09
  • Be aware that one big difference between Rust and C/C++ is that in C, arguments are by default pass-by-copy; whilst in Rust, arguments are pass-by-move. No clone ever gets made in Rust unless explicitly instructed. How many copies add in your C++ code performs is optimization dependent. In Rust no clone is performed unless explicitly requested. That on the contrary means every object in Rust is trivially movable, whilst in C/C++ you need a move constructor. Which has some other implications like: self-referential structs in Rust are impossible. Irrelevant to your question though. – Finomnis Commented Feb 2 at 8:58
Add a comment  | 

2 Answers 2

Reset to default 2

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.canvas.copy(&txt, src.clone(), dst.clone())

See What is the difference between Copy and Clone?

Let me fully reproduce your error, hopefully matching your problem:

pub struct Rect;
struct Texture;

fn canvas_copy<R1, R2>(_txt: &Texture, _src: R1, _dst: R2)
where
    R1: Into<Option<Rect>>,
    R2: Into<Option<Rect>>,
{
}

fn get_or_load_and_use<F>(mut f: F) -> Result<(), String>
where
    F: FnMut(&Texture),
{
    let txt = Texture;
    f(&txt);
    Ok(())
}

pub fn copy_checked<R1, R2>(src: R1, dst: R2) -> Result<(), String>
where
    R1: Into<Option<Rect>>,
    R2: Into<Option<Rect>>,
{
    get_or_load_and_use(
        // closure arg here
        |txt: &Texture| canvas_copy(&txt, src, dst),
    )
}
error[E0507]: cannot move out of `src`, a captured variable in an `FnMut` closure
  --> src\lib.rs:27:43
   |
20 | pub fn copy_checked<R1, R2>(src: R1, dst: R2) -> Result<(), String>
   |                             --- captured outer variable
...
27 |         |txt: &Texture| canvas_copy(&txt, src, dst),
   |         ---------------                   ^^^ move occurs because `src` has type `R1`, which does not implement the `Copy` trait
   |         |
   |         captured by this `FnMut` closure
   |
help: if `R1` implemented `Clone`, you could clone the value
  --> src\lib.rs:20:21
   |
20 | pub fn copy_checked<R1, R2>(src: R1, dst: R2) -> Result<(), String>
   |                     ^^ consider constraining this type parameter with `Clone`
...
27 |         |txt: &Texture| canvas_copy(&txt, src, dst),
   |                                           --- you could clone this value

error[E0507]: cannot move out of `dst`, a captured variable in an `FnMut` closure
  --> src\lib.rs:27:48
   |
20 | pub fn copy_checked<R1, R2>(src: R1, dst: R2) -> Result<(), String>
   |                                      --- captured outer variable
...
27 |         |txt: &Texture| canvas_copy(&txt, src, dst),
   |         ---------------                        ^^^ move occurs because `dst` has type `R2`, which does not implement the `Copy` trait
   |         |
   |         captured by this `FnMut` closure
   |
help: if `R2` implemented `Clone`, you could clone the value
  --> src\lib.rs:20:25
   |
20 | pub fn copy_checked<R1, R2>(src: R1, dst: R2) -> Result<(), String>
   |                         ^^ consider constraining this type parameter with `Clone`
...
27 |         |txt: &Texture| canvas_copy(&txt, src, dst),
   |                                                --- you could clone this value

The reason this fails has actually nothing to do with how the Rust compiler resolves different closures, but instead the problem is the FnMut. You take src and dst in your canvas.copy as values, so you can only call canvas.copy once. Yet you specify that your closure is FnMut, meaning it can be called multiple times. A closure that consumes a captured value does not implement FnMut, as outlined in another answer of mine here.

So to fix your code, you have several options:

  • Specify the closure type as FnOnce:
pub struct Rect;
struct Texture;

fn canvas_copy<R1, R2>(_txt: &Texture, _src: R1, _dst: R2)
where
    R1: Into<Option<Rect>>,
    R2: Into<Option<Rect>>,
{
}

fn get_or_load_and_use<F>(f: F) -> Result<(), String>
where
    F: FnOnce(&Texture),
{
    let txt = Texture;
    f(&txt);
    Ok(())
}

pub fn copy_checked<R1, R2>(src: R1, dst: R2) -> Result<(), String>
where
    R1: Into<Option<Rect>>,
    R2: Into<Option<Rect>>,
{
    get_or_load_and_use(
        // closure arg here
        |txt: &Texture| canvas_copy(&txt, src, dst),
    )
}
  • Make src and dst clonable and clone them:
pub struct Rect;
struct Texture;

fn canvas_copy<R1, R2>(_txt: &Texture, _src: R1, _dst: R2)
where
    R1: Into<Option<Rect>>,
    R2: Into<Option<Rect>>,
{
}

fn get_or_load_and_use<F>(mut f: F) -> Result<(), String>
where
    F: FnMut(&Texture),
{
    let txt = Texture;
    f(&txt);
    Ok(())
}

pub fn copy_checked<R1, R2>(src: R1, dst: R2) -> Result<(), String>
where
    R1: Into<Option<Rect>> + Clone,
    R2: Into<Option<Rect>> + Clone,
{
    get_or_load_and_use(
        // closure arg here
        |txt: &Texture| canvas_copy(&txt, src.clone(), dst.clone()),
    )
}
  • Make src and dst Copy, which will change how functions are called. Function arguments with Copy will no longer be pass-by-move, but instead be pass-by-copy:
pub struct Rect;
struct Texture;

fn canvas_copy<R1, R2>(_txt: &Texture, _src: R1, _dst: R2)
where
    R1: Into<Option<Rect>>,
    R2: Into<Option<Rect>>,
{
}

fn get_or_load_and_use<F>(mut f: F) -> Result<(), String>
where
    F: FnMut(&Texture),
{
    let txt = Texture;
    f(&txt);
    Ok(())
}

pub fn copy_checked<R1, R2>(src: R1, dst: R2) -> Result<(), String>
where
    R1: Into<Option<Rect>> + Copy,
    R2: Into<Option<Rect>> + Copy,
{
    get_or_load_and_use(
        // closure arg here
        |txt: &Texture| canvas_copy(&txt, src, dst),
    )
}

I would probably go with the FnOnce version, because if you only call the function once, there's no reason to use a more restrictive type. Some recommendations regarding this can be found in this answer of mine.

The FnOnce is probably also what you had in mind when you made the statement:

If it was like C++, then there should be no need to copy the args. The function would just be inlined compiled in.

This is exactly how FnOnce behaves in this situation.


Another solution would be to change the signature of canvas.copy, although it's unclear if that's possible or not; it seems from your code like this could be an external library function.

If you can, you can resolve all of this by simply taking the values by reference instead:

pub struct Rect;
struct Texture;

fn canvas_copy<R1, R2>(_txt: &Texture, _src: &R1, _dst: &R2)
where
    R1: Into<Option<Rect>>,
    R2: Into<Option<Rect>>,
{
}

fn get_or_load_and_use<F>(mut f: F) -> Result<(), String>
where
    F: FnMut(&Texture),
{
    let txt = Texture;
    f(&txt);
    Ok(())
}

pub fn copy_checked<R1, R2>(src: &R1, dst: &R2) -> Result<(), String>
where
    R1: Into<Option<Rect>>,
    R2: Into<Option<Rect>>,
{
    get_or_load_and_use(
        // closure arg here
        |txt: &Texture| canvas_copy(&txt, src, dst),
    )
}

Which, in my opinion, would be even cleaner. Still, I would make the FnMut an FnOnce if you only call it once; that's just good API design:

pub struct Rect;
struct Texture;

fn canvas_copy<R1, R2>(_txt: &Texture, _src: &R1, _dst: &R2)
where
    R1: Into<Option<Rect>>,
    R2: Into<Option<Rect>>,
{
}

fn get_or_load_and_use<F>(f: F) -> Result<(), String>
where
    F: FnOnce(&Texture),
{
    let txt = Texture;
    f(&txt);
    Ok(())
}

pub fn copy_checked<R1, R2>(src: &R1, dst: &R2) -> Result<(), String>
where
    R1: Into<Option<Rect>>,
    R2: Into<Option<Rect>>,
{
    get_or_load_and_use(
        // closure arg here
        |txt: &Texture| canvas_copy(&txt, src, dst),
    )
}

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论