7

With this code:

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

struct Rectangle {
    p1: Point,
    p2: Point,
}

impl Rectangle {
    pub fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Rectangle {
        let r = Rectangle {
            p1: Point { x: x1, y: y1 },
            p2: Point { x: x2, y: y2 },
        };
        // some code where r is used
        r
    }
}

let rectangle = Rectangle::new(0.0, 0.0, 10.0, 10.0);

From a memory point of view, is rectangle the same instance as r, or is it a copy of r?

Do I have to explicitly return by reference (something like &r)?

I have to create millions of rectangles, and I don't want there to be useless copies.

ljedrz
  • 20,316
  • 4
  • 69
  • 97
barbacan
  • 632
  • 6
  • 16

1 Answers1

23

From a memory point of view, is rectangle the same instance as r, or is it a copy of r?

Unspecified.

The Rust language specifies the semantics of the language, and while they do constrain the implementation somewhat, in this case they do not. How a return value is passed up the call stack is part of the ABI, and not only is the ABI unstable (in Rust), it's also platform specific.

Do I have to explicitly return by reference (something like &r)?

Returning by reference is not possible.

You could return a Box<Rectangle> but the cost of the memory allocation would dwarf the cost of copying a Rectangle in the first place, so it's hardly advisable.

You could force this using output parameters instead, but this has other issues:

  • if you have a &mut Rectangle parameter, you first need to have a valid instance, which has to be initialized; rather wasteful,
  • if you have a *mut Rectangle pointing to uninitialized memory, you need to use unsafe code, hardly satisfying.

However...

I have to create millions of rectangles, and I don't want there to be useless copies.

I think you are worrying for nothing.

The first rule of performance tuning is measure first; and I doubt that you'll be able to observe a performance issue in the creation of those millions of rectangles.

The compiler has multiple tricks up its sleeves, such as:

  • not even materializing the rectangle instance to start with, but pass its components via CPU registers instead,
  • inlining new at the call site, avoiding any copy whatsoever,
  • ...

Thus, before worrying about the cost of copying 4 f64, I would implement the naive solution, compile in release mode, and observe what happens.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • Does Rust gurantee calling `drop()` to free up resources of an original value when copying has "de facto" taken place? – Ilya Loskutov Mar 25 '21 at 18:48
  • 1
    @Mergasov: I think your mental model is off. There are two operations in Rust, identified by two traits: `Clone` and `Copy`. When you create a `clone`, you (the user) specify the logic involved in creating the clone; as an example, for a `String` it means allocating a second buffer and copying the contents of the first there. After cloning, both instances live their lives independently from one another. If you don't need to type `.clone()`, Rust just _moved_ the instance. If an instance is moved, it is not dropped. If an instance is copied (`Copy`), dropping is a no-op. – Matthieu M. Mar 26 '21 at 10:28
  • 1
    @Mergasov: And therefore, the _reverse_ is guaranteed. Rust guarantees that when it _bit copies_ a value (whether moving or `Copy`ing) it will never call `Drop`. – Matthieu M. Mar 26 '21 at 10:28