Custom copy
Between "the compiler copies each field" and "no copies at all" there is a third choice: write the copy yourself. Give = a body, and every copy of the type runs it.
In real code the body does what a field-by-field copy cannot — typically duplicating something the value owns, so the copy gets its own instead of sharing. A buffer that owns a block of memory, for example, would allocate a fresh block and copy the bytes into it. Here each copy of a sheet of paper is a photocopy, and remembers how many generations it is from the original.
Three ways to copy
In extend T | What a copy does | Lesson |
|---|---|---|
| nothing | copies each field | Copy |
func =(self: &var T, other: &T); | nothing — copying is an error | Non-copyable types |
func =(self: &var T, other: &T) { … } | runs your body | this lesson |
Writing the copy
struct Sheet {
text: char8[..];
generation: int32;
}
extend Sheet {
func =(self: &var Sheet, other: &Sheet) {
self.text = other.text;
self.generation = other.generation + 1;
PrintLine(" photocopy: generation {} -> {}", other.generation, self.generation);
}
func ~Sheet(self: &var Sheet) {
PrintLine(" shredding generation {}", self.generation);
}
}
other is the value being copied, borrowed read-only so the copy cannot change or consume it. self is the new value to fill in. Once you write the body, the field-by-field copy is gone: every field you want in the copy, you set yourself. Leave out self.text = other.text; and every photocopy comes out with empty text.
The destructor is there to show that every copy is a value of its own, with its own end.
Every copy runs it
A binding copies:
let original = Sheet { text: "Minutes", generation: 1 };
let copy = original;
copy is generation 2. original is untouched, still generation 1.
Passing by value copies too. Show takes its sheet by value, so the caller's sheet is photocopied into the parameter, and the photocopy is shredded when Show returns:
func Show(sheet: Sheet) {
PrintLine(" showing '{}', generation {}", sheet.text, sheet.generation);
}
passing by value:
photocopy: generation 2 -> 3
showing 'Minutes', generation 3
shredding generation 3
Assigning over a live value
Assignment over a value that already exists does two things, in a fixed order — it makes the new copy first, then ends the old value's life:
var board = Sheet { text: "Agenda", generation: 1 };
board = copy;
flowchart LR
c["copy<br/>generation 2"] -- "first, = runs:<br/>a photocopy" --> n["new sheet<br/>generation 3"]
b["board<br/>generation 1"] -- "then the old value ends" --> s["shredding<br/>generation 1"]
n -- "finally the new one<br/>is installed" --> b2["board<br/>generation 3"]The output shows the order: the photocopy line comes before the shredding line.
Only a copy runs =
= runs when an existing value is copied — one that keeps its value afterwards. A sheet made on the spot is not a copy of anything, so it moves straight in with no photocopy. That is why var board = Sheet { … }; prints nothing, and why board = Sheet { … }; would print only the shredding of the old sheet. A value handed over with <- is not copied either.
| Written | Runs =? |
|---|---|
let copy = original; | yes |
Show(copy) | yes |
board = copy; | yes |
var board = Sheet { … }; | no — made on the spot |
Show(<-copy) | no — moved |
At the end of Main, three sheets are still alive — original, copy and board — and each is shredded once, newest first: generation 3, then 2, then 1.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// Between "the compiler copies each field" and "no copies at all" there is a third choice: write
// the copy yourself. Give `=` a body and every copy of the type runs it. `other` is the value
// being copied, borrowed read-only so the copy cannot change or consume it, and `self` is the
// new value to fill in.
//
// In real code the body does what a field-by-field copy cannot, typically duplicating something
// the value owns so the copy gets its own instead of sharing. Here each copy of a sheet is a
// photocopy and remembers how many generations it is from the original. The destructor shows
// that every copy is a value of its own, with its own end.
import Io::PrintLine;
struct Sheet {
text: char8[..];
generation: int32;
}
extend Sheet {
func =(self: &var Sheet, other: &Sheet) {
self.text = other.text;
self.generation = other.generation + 1;
PrintLine(" photocopy: generation {} -> {}", other.generation, self.generation);
}
func ~Sheet(self: &var Sheet) {
PrintLine(" shredding generation {}", self.generation);
}
}
// Taken by value, so the caller's sheet is copied into `sheet` by the custom copy.
func Show(sheet: Sheet) {
PrintLine(" showing '{}', generation {}", sheet.text, sheet.generation);
}
func Main() -> int {
PrintLine("binding:");
let original = Sheet { text: "Minutes", generation: 1 };
let copy = original;
PrintLine(" original {}, copy {}", original.generation, copy.generation);
PrintLine("passing by value:");
Show(copy);
// Assigning over a live value makes the new copy first, then ends the old value's life.
// Only a copy of an existing value runs `=`. A sheet made on the spot is not a copy of
// anything, so it moves straight in with no photocopy, here and in `board = Sheet { ... };`.
PrintLine("assigning:");
var board = Sheet { text: "Agenda", generation: 1 };
board = copy;
PrintLine(" board now holds '{}', generation {}", board.text, board.generation);
PrintLine("end of Main:");
return 0;
}
Run it
cd Examples/Ownership/CustomCopy
rux run
binding:
photocopy: generation 1 -> 2
original 1, copy 2
passing by value:
photocopy: generation 2 -> 3
showing 'Minutes', generation 3
shredding generation 3
assigning:
photocopy: generation 2 -> 3
shredding generation 1
board now holds 'Minutes', generation 3
end of Main:
shredding generation 3
shredding generation 2
shredding generation 1
Common mistakes
other by value.func =(self: &var Sheet, other: Sheet) fails with error: copy special operation for type 'Sheet' must have signature 'func =(self: &var Sheet, other: &Source)'. Taking other by value would itself need a copy — the very operation being defined.other.generation = 0; inside the body fails with error: cannot modify data through immutable reference '&Sheet'. A copy must leave its source exactly as it was.= to run for every assignment.board = Sheet { text: "Notes", generation: 5 }; runs no photocopy: the new sheet is made on the spot and moves straight in. Only the old board is shredded. If your body counts or logs copies, values built in place will not show up in the count.Try it yourself
- Add
let third = copy;after the binding. Predict every photocopy and shredding line before you run it. - Change
Show(copy)toShow(<-copy). The photocopy line disappears — and a later line no longer compiles. Which one, and why? - Remove
self.text = other.text;from the body and run the program. What does each photocopy say it is showing?
Learn more
- Copy — the copy the compiler writes for you
- Non-copyable types — the bodyless
= - Destructor — the other end of every copy's life