Which problems does owning-ref solve?

Take

let s: Box<str> = "   Hello   ".into();

box_str

Now, what does trimming do?

let trimmed = s.trim(); /* str::trim(&*s) */

box_str_trimmed

Now, all is good, except for one thing: you cannot move s while trimmed exists!! The reason for that is that you shouldn't be able to move the characters around /
free the allocated characters while trimmed exists: so trimmed cannot outlive s, and more generally, the contents s points to should not be mutated while trimmed exists.

This is why we call this a borrow: for the time trimmed lives (with NLL, the time trimmed is used), a duration that we can call / denote 'a, that is, the lifetime 'a, s cannot access the contents it points to since they have been borrowed by trimmed.

box_str_trimmed_borrows

Now, the problem is, that Rust cannot make the difference between s itself (i.e., the ptr, len pair) and the "contents s points to". So, while it is safe to move the ptr, len pair around, Rust does not know that, and forbids it (example).

The solution most people use then is to copy the trimmed contents elsewhere, so that they are not tied to s anymore:

let trimmed: String = s.trim().to_string(); // or
let trimmed: String = s.trim().to_owned(); // or
let trimmed: Box<str> = s.trim().into();
  • (the only difference between a String and a Box<str> is a third field, capacity: usize, which does not really play any role in my example, so I will stick to Box<str> for the sake of simplicity)

  • Playground

As you can see, we no longer borrow from s, at the cost of duplicating / copying the "Hello" string elsewhere in the heap: we have reallocated "Hello"

Doing this just to satisfy the borrow checker is quite saddening.

The solution of owning-ref or a custom implementation based on Pin<Box<..>>, is that, since moving s does not move the contents it points to, and by forbidding mutation of the pointee (you will note that there is no DerefMut what so ever for OwningRef<_>), if we manage to ensure that the reference (trimmed) does not outlive s, then all should be fine (Rust can still not know that, so unsafe is required). And this is achieved by stitching the reference and s together in a single struct: OwningRef.

It starts off with a dummy reference to the whole contents (e.g., " Hello " with the trailing spaces), in which case it "wastes" a little bit more stack memory than a simple Box<str>, but then, when using the .map() method, we can manipulate that reference and "shrink" it at will. And the magic is that whenever we want to access / read the contents the reference points to, Rust uses the Deref trait, which has been overloaded to use the shrunk ref instead of the owning variable.

7 Likes