Ownership · Lesson 11.5

Non-copyable types

Source
Prohibit copying a type with a bodyless func =, so that it can only be moved.
You'll need: Destructor, Move

A hotel key stands for a room. If keys could be copied, two of them would each be handed back at checkout — the destructor would run twice for one room. For a type like this, the compiler's automatic copy is wrong, and Rux lets the type turn it off.

Once copying is off, the only way to hand a key on is to move it. One key, one owner, one destructor call — and the compiler checks all three.

Turning copying off

The copy operator is =. Declaring it inside extend with no body says "this type has no copy":

struct RoomKey {
    room: int32;
}

extend RoomKey {
    // No body: copying is prohibited.
    func =(self: &var RoomKey, other: &RoomKey);

    func ~RoomKey(self: &var RoomKey) {
        PrintLine("    key {} handed back", self.room);
    }
}

The signature is the one a real copy would have — self is the new value to fill in, other the value being copied — but with a ; where the body would be. The Custom copy lesson gives it a body.

Moving instead

From then on every use that would copy a key is an error, and the diagnostic shows the move to write instead:

let spare = key;
error: move-only value 'key' requires an explicit '<-' in initialization
  note: plain by-value use copies its source, but 'RoomKey' prohibits copying
  help: write 'let destination <- key' to transfer ownership

With the arrow, the key changes hands and key is finished:

let friend <- key;
PrintLine("    friend holds key {}", friend.room);

Passing by value is a copy too, so an argument needs the arrow as well. CheckOut takes ownership of the key, so the key's life ends when the function returns:

func CheckOut(key: RoomKey) {
    PrintLine("    checking out of room {}", key.room);
}
CheckOut(<-friend);
PrintLine("    done");

That is why the output shows "key 12 handed back" between "checking out of room 12" and "done": the destructor runs as CheckOut returns, not at the end of Main.

Fresh keys and borrowed keys

Two things still work without an arrow. A value made on the spot has no old owner, so CheckIn returns its new key with a plain return:

func CheckIn(room: int32) -> RoomKey {
    PrintLine("    key {} issued", room);
    return RoomKey { room: room };
}

And a borrow is not a copy. A function that only needs to look at a key takes &RoomKey, and the caller keeps the key.

flowchart LR
    k(["key: RoomKey<br/>no copy"]) -- "let spare = key" --> x["error: requires<br/>an explicit '<-'"]
    k -- "let friend <- key" --> f["friend owns the key<br/>key is finished"]
    k -- "Peek(key) with key: &RoomKey" --> b["borrowed for the call<br/>key still owns it"]

Everywhere a copy would happen

Every by-value use of a named key needs the arrow. The compiler names the place in each error:

WhereRefusedWritten with a move
A bindinglet spare = key;let spare <- key;
An argumentCheckOut(key)CheckOut(<-key)
A returnreturn key;return <-key;
A struct fieldBooking { key: key }Booking { key: <-key }
An assignmentkey = other;key <- other;

The prohibition also spreads outwards. A struct with a RoomKey field cannot be copied either — copying it would copy the key — so the compiler treats the whole struct as move-only.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// A hotel key stands for a room. If keys could be copied, two of them would each be handed back
// at checkout: the destructor would run twice for one room. For a type like this the compiler's
// automatic copy is wrong, and Rux lets the type turn it off.
//
// The copy operator is `=`. Declaring it with no body says "this type has no copy":
//
//     func =(self: &var RoomKey, other: &RoomKey);
//
// From then on every use that would copy a key is an error, and the only way to hand one on is
// to move it with `<-`. One key, one owner, one destructor call.
import Io::PrintLine;

struct RoomKey {
    room: int32;
}

extend RoomKey {
    // No body: copying is prohibited.
    func =(self: &var RoomKey, other: &RoomKey);

    func ~RoomKey(self: &var RoomKey) {
        PrintLine("    key {} handed back", self.room);
    }
}

func CheckIn(room: int32) -> RoomKey {
    PrintLine("    key {} issued", room);
    return RoomKey { room: room };
}

// Takes ownership of the key, so the key's life ends when this function returns.
func CheckOut(key: RoomKey) {
    PrintLine("    checking out of room {}", key.room);
}

func Main() -> int {
    PrintLine("check in:");
    let key = CheckIn(12);

    // A copy is refused, and the diagnostic shows the move to write instead:
    //
    //     let spare = key;
    //
    // error: move-only value 'key' requires an explicit '<-' in initialization
    //   note: plain by-value use copies its source, but 'RoomKey' prohibits copying
    //   help: write 'let destination <- key' to transfer ownership

    PrintLine("give the key to a friend:");
    let friend <- key;
    PrintLine("    friend holds key {}", friend.room);

    // Passing by value is a copy too, so the argument needs the arrow as well. Without it:
    // error: move-only value 'friend' requires an explicit '<-' in argument
    PrintLine("check out:");
    CheckOut(<-friend);
    PrintLine("    done");
    return 0;
}

Run it

cd Examples/Ownership/NoCopy
rux run
check in:
    key 12 issued
give the key to a friend:
    friend holds key 12
check out:
    checking out of room 12
    key 12 handed back
    done

Common mistakes

Binding or passing a key without the arrow.
let spare = key; fails with error: move-only value 'key' requires an explicit '<-' in initialization, and CheckOut(friend) with error: move-only value 'friend' requires an explicit '<-' in argument. Both notes say the same thing: plain by-value use copies its source, and this type prohibits copying.
Returning a named key with a plain return.
A function that builds a key in a local and ends with return key; fails with error: move-only value 'key' requires an explicit '<-' in return. Write return <-key;. Only a value made on the spot, like return RoomKey { room: room };, needs no arrow.
Replacing a key with =.
With two var keys, key = other; fails with error: copying type 'RoomKey' is prohibited. Assignment copies too. Write key <- other;, which hands back the old key first and then moves other in.

Try it yourself

  1. Write func Peek(key: &RoomKey) that prints the room number, and call it twice on key before giving it to the friend. Why does Peek need no arrow?
  2. Write func Spare(room: int32) -> RoomKey that stores a new key in a let before returning it. Which return does it need?
  3. Make two var keys and replace the first with key <- other;. When is each key handed back?
  4. Declare struct Booking { guest: char8[..]; key: RoomKey; }, build one, and try to copy it with =. What does the error say about Booking?

Learn more

  • Move — the arrow in all three places
  • Custom copy — giving = a body instead
  • Partial move — taking a struct of move-only fields apart