7

I can't work out how to have clean looking maths on structs without requiring that those struct values be copied everywhere.

If you wanted to have a struct that you could perform math on, you'd write something like this:

use std::ops::*;

struct Num {
    i: i32,
}

impl Add for Num {
    type Output = Num;
    fn add(self, other: Num) -> Num {
        Num {
            i: self.i + other.i,
        }
    }
}

(This is a simplified example. An actual example might be doing Vector maths)

This lets us write nice a + (b / (c * d)) style code.

Due to borrowing semantics, the above code falls over as quickly as a + b + a. Once a is used once it can't be used again, as ownership was moved to the relevant function (i.e. add).

The simple way of solving this is to implement Copy for the struct:

#[derive(Copy)]
struct Num {
    i: i32,
}

This means when Nums are passed to add, their values are automatically cloned so that they can be dropped cleanly.

But this seems inefficient! We don't need these structs to be duplicated all over the place: it's read-only, and we really just need it to be referenced to create the new structure we're returning.

This leads me to think that we should implement the math operations on references instead:

impl<'a> Add for &'a Num {
    type Output = Num;
    fn add(&'a self, other: &'a Num) -> Num {
        Num {
            i: self.i + other.i,
        }
    }
}

Now we have math operations where we aren't cloning data all over the place, but now our math looks gross! a + (b / (c * d)) now must be &a + &(&b / &(&c * &d)). It doesn't help if you have your value type references either (eg let a = &Num { /* ... */ }), because the return value of add is still a Num.

Is there a clean way to implement ops for structs such that math operations look clean, and the struct values aren't being copied everywhere?

Related:

SCdF
  • 57,260
  • 24
  • 77
  • 113

1 Answers1

4

No; the traits consume by value, there's no way around that.

But this seems inefficient! We don't need these structs to be duplicated all over the place: it's read-only, and we really just need it to be referenced to create the new structure we're returning.

I wouldn't worry about the efficiency of copying a single integer. That's what computers do. In fact, a reference would likely be slower as a reference is also basically an integer which has to be copied and then a piece of memory has to be looked up, also copying the referred-to integer into registers.

I'm obvious[ly] not copying a single integer!

An actual example might be doing Vector maths

Then the problem becomes one of confusing your users. Without looking at the implementation, how could a user know that a + a is "lightweight" or not. If you, the implementer of your type, knows that it's lightweight to copy, you mark it Copy. If it's not, then references need to be made.


That's the situation today. There is some experimental work that might indeed make this a little bit nicer in the future:

imagine never having to write [...] let z = &u * &(&(&u.square() + &(&A * &u)) + &one); again

This experiment spawned from a now-deferred RFC.

As an amusing aside, this ugly syntax is referred to as the Eye of Sauron.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • So if you were trying to be conscious of performance here, what would you do? Is the compiler smart enough to know it doesn't actually need to duplicate data? Would `#[inline(always)]` improve anything? – SCdF Mar 01 '18 at 23:07
  • 3
    "I wouldn't worry about the efficiency of copying a single integer." I'm obvious **not copying a single integer!** This is a short example for stack overflow. – SCdF Mar 01 '18 at 23:20
  • I wonder if the value copied would be effectively a single `i32`, or struct `Num` would be larger. That is, is this particular abstraction free. – 9000 Mar 01 '18 at 23:20
  • @9000 Within the Rust ABI, it would be completely free (this is called a newtype). Each external ABI has its own guarantees and it can change depending on what you compile for. – Shepmaster Mar 01 '18 at 23:33
  • @Shepmaster nice! I presumed someone was working on it. I wondered if you could also do it with macros (you'd have to mark / wrap every function that did the math) but I'm not familiar enough with how much Rust macros can get away with – SCdF Mar 02 '18 at 08:27
  • 1
    @SCdF there is [unsauron](https://github.com/mystor/unsauron), but it doesn't appear to be maintained. – Shepmaster Mar 02 '18 at 14:23