What is the intent for ops::Add<Rhs=Self> type constraint?

Hi, I'm a newbie!

I was wondering why this Playground*edited link code didn't compile, so I wrote an SE post and now I want to know why "the book" the std::ops documentation on operator overloading implement add for type Point with moves instead of immutable borrows for the arguments.

Why does the guide suggest we implement add for Point with argument self and Point like below instead of with &self and &Point? What is the benefit of one over the other? I suppose there's also the Add<Rhs=&Point> for Point but one thing at a time unless that really explains the reasoning this move implementation is in the book suggested by the docs.

use std::ops::Add;

struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

It does compile. Did you mean this, from the SE post? In short, because it doesn't match the trait. The method takes self, Rhs for Add<Rhs>, and not &self, &Rhs.

I'm not sure what part of The Book you're referring to, but The Book documents the language and standard library, it doesn't dictate them. It's probably just documenting how the trait is designed.

As for why trait is designed like that in the first place, it's for heavier types, such as those that allocate -- for example String. [1] Due to the by-value design, you can reuse the allocation of the left operand.

You can also implement Add for &Ty. But another consideration is that + doesn't auto-ref, so for Copy types, you would also lose ergonomics by having to explicitly reference if only Add for &Ty is implemented.


  1. Or, say, some big integer type. ↩ī¸Ž

2 Likes

Thanks for pointing out the issue with the playground link, I also realized I was looking at other parts of the docs so I did my best to indicate within the "no more than 2 links" rule because I'm new.

I'm actually new enough where I didn't know that about String's add. I messed around with an example and I looked at the four cases of where the references could go and I saw the compiler text,

note: calling this operator moves the left-hand side
--> /rustc/90c541806f23a127002de5b4038be731ba1458ca/library/core/src/ops/arith.rs:92:12

and I suppose it's a general theme to do this, not just within String. So now, I'm comfortable with that. However, is this part of a general theme of, "a function call will explicitly appear immutable if guarantees immutable?" opposed to C++'s "will cast to what is implemented, on dev reading and writing to know what that is"?

I see the point on the ergonomics, especially after implementing all 4. I also looked a bit into #[const_trait] on the Add trait but that was confusing for my level of current understanding. If you'd like to make a remark on that, I'd appreciate it and maybe I'll come back to it in a month or two.

I don't really understand the question, and the context of operator overloading and being generic is probably clouding things. But very generally, what types a function or method takes is determined by the signature, and in this case, Add::add takes its generic parameters (Self and Rhs) by value. You can mutate things you own, so in the case of Self = String, it's free to modify the String (and then return it).

A mini-dump of what might be confusing follows; I'm not sure if it will help or just throw too much information out there and hurt, so use your own judgement.


One potential source of confusion is that Rust references are full blown types. So it's possible that one or both of the generic parameters is a reference, such as Rhs = &'_ str for the String implementation.

Another potential source of confusion is that &'_ T and &'_ mut T are two very different types, whereas the mut you see in simple bindings like

let mut i = 0;
let j = 0;
fn foo(mut i: i32, j: i32) {}

don't change the type of anything; i and j are the same type, but you can't overwrite or take a &mut of i without rebinding it. But since you own i, you can rebind it by moving it:

let mut i = i;

So it's effectively just a lint.


a + b is notionally a function call like <_ as Add<_>>::add(a, b). [1]

Function arguments support coercions, but there are some subtleties when multiple generic implementations exist; I think coercions are effectively disabled then. When coercions are in play, a &mut T can coerce to a &T, for example, and deref coercion can coerce a &String or &&&str into a &str.


It's an internal implementation detail you can ignore.

(There's no stable way to declare a trait implementation to be const, that is, executable at compile time. But it's an eventual goal of the language and there's some (incomplete) experimental support on nightly. The attribute almost surely has something to do with that.)


  1. There's some corner cases I won't dig up which you're unlikely to ever notice. ↩ī¸Ž

1 Like

I don't really understand what is being asked here. The error you were getting has nothing to do with mutability. It's a simple type mismatch.

I don't understand what you mean by "explicitly appear immutable" or what is a contrasting piece of C++ code that would "cast to what is implemented" (?). Rust never automatically removes immutability, and function arguments must (for the most part) exactly match the declared types; there's nothing funky going on – this is exactly why your original example didn't compile, so I find it puzzling why you would ask something like this.

You're right about this, and I was confused by it at the start since I didn't understand that the was the Rhs type. But that got identified in my SE post, and my concerns were with the content of the docs. I wanted to know why the example provided would implement add with a move for both args instead of immutable reference.

@quinedot pointed out that this is the behavior of string. add "... moves the left-hand side" is something I didn't expect. So after that, it started to make more sense to me about the usage of add operations.

  • I don't want to mutate either of them
  • I won't often need BOTH values for later use after the operation

These could motivate the idea to move the LHS of + and use that memory for the result. Then my next question was along the lines of, "What might I want T + T to do if I only implemented T + &T?"

Analogous C++ would allow either a reference type or a const reference type to work with the implemented add - but really, I've no way to directly call the implemented + operation with syntax that's explicit in the line.

However, in Rust, I can say, "I want to add a value to an immutable reference" in line with &T. And if that's not implemented (or not coercible, which I'm less clear on but &mut T where &T is implemented seems fair game to me), then the designers of Rust would prefer I not do that and disallow it with the compiler.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.