Overloaded operators that don't consume their arguments

The function below works, but it consumes its arguments. Is there some way to write an operator function that just borrows its arguments? Preferably without having to write

let x = (&y) * (&z);

to invoke the operator.

/// Multiply by matrix, V*M form. Rotates, scales, translates vector by matrix
impl ops::Mul<LLMatrix4> for LLVector3 {
    type Output = LLVector3;
    fn mul(self, rhs: LLMatrix4) -> LLVector3 {
    // Operate "to the left" on row-vector a
        LLVector3 {
        x:  self.x * rhs.m[0][0] + 
            self.y * rhs.m[1][0] + 
            self.z * rhs.m[2][0] +
            1.0 * rhs.m[3][0],

        y: self.x * rhs.m[0][1] + 
            self.y * rhs.m[1][1] + 
            self.z * rhs.m[2][1] +
            1.0 * rhs.m[3][1],

        z:  self.x * rhs.m[0][2] + 
            self.y * rhs.m[1][2] + 
            self.z * rhs.m[2][2] +
            1.0 * rhs.m[3][2],
        }
    }
}

Lang is interested in doing something around this (Tracking issue for experiments around coercions, generics, and Copy type ergonomics · Issue #44619 · rust-lang/rust · GitHub) but there's nothing happening right now and it's unknown what the right solution would be.

1 Like

There may be a nice way to do this, by viewing it as an optimization. See playground.

This is the 4x4 matrix multiply. But now the matrix type has a derived "Copy". That allows pass by value, so the operator doesn't consume its arguments, and, as seen here, one of them is reused. If "Copy" is not available, the error

error[E0382]: use of moved value: `m1`

is reported. That gives us ergonomics for the matrix multiply operator, at a cost in performance for the copy.

Or is the compiler smart enough to detect that it doesn't need to copy the function arguments here? If the function doesn't modify or return an argument, it need not be copied. It's the same check as for an immutable borrow. If an immutable borrow would be allowed, the copy can be optimized out. Is that being done already?

If that's being done already, just tell users it's being done and the problem is solved.

Officially saying that an optimization is done is committing to guaranteeing and maintaining that optimization, and I doubt the compiler team is willing to sacrifice the flexibility to make such a commitment -- or even able to reasonably make such a commitment, as it probably depends on LLVM for most people, or Cranelift, or whatever backend is used.

So the standard advice is "measure and see if it even matters". And/or "check the output to see if the optimization is done". And if it matters, get it into the state you want today, and write some tests to help ensure it stays that way. Sometimes #[inline] or its variants can help. Other times, you think they'll help but they hurt. And sometimes you take the ergonomic hit for the sake of performance or design.

As for your specific example, it inlined identity() a few times when I tried it, instead of reusing the results of identity(). If I put #[inline(never)] on identity(), it calls identity() once and reuses the results. If I put #[inline(always)] on the multiplication method instead, the results of the multiplications get calculated statically and baked into the binary, and the main function just prints that. But results will certainly vary call site to call site (and across compiler versions, target architectures, etc.).

1 Like

It's like tail call recursion, I guess. Rust doesn't guarantee that, either.

The example just uses identity() because I needed something to generate a dummy matrix for an example. That's not a good test for optimizations, since it's a constant.