Continuing on from Chs 5 & 6 of “The Book” (see “Reading Club” post here), I don’t think the Copy
and Clone
traits were quite given sufficient coverage.
So I thought I’d post my understanding here.
Tl;Dr
Copy | Clone |
---|---|
Implicit | Explicit (v.clone() ) |
Simple (memcpy ) |
Arbitrary |
“cheap”/quick | Potentially expensive |
Simple types | Any type |
Derivable Traits
- These are traits which have a standard implementation that can be freely and easily applied to
structs
andenums
(IE, custom types). - The ones available in the standard library are listed in Appendix C of The Book. Other libraries/crates can apparently implement them too (though I don’t know of any).
- They are part of and use the
Attributes
syntax (see Documentation in the Rust Reference here):#[ATTRIBUTE]
. - To derive a trait we write
#[derive(TRAIT1, TRAIT2, ...)]
above our type declaration
#[derive(Copy, Clone)]
struct Copyable {
field: i32,
}
#[derive(Clone)]
struct OnlyClonable {
field: i32,
}
The Three Step Ladder
- The two traits,
Clone
andCopy
, are about providing opt-in options for custom types around what “ownership semantics” they obey. - “move semantics”: are what we know for most types and have learnt about in ch 4. There is no copying, only the moving of ownership from one stack to another or the use of references and the “borrow checker” that verifies safety.
- “Copy semantics”: are what basic types like
i32
enjoy. No ownership movements. Instead, these variables get implicitly copied, because they’re simple and copying involves the same work involved in passing a memory address around.
The RFC for these traits puts it nicely …
From RFC 0019 - Opt in built-in traits document on the Copy Trait
Effectively, there is a three step ladder for types:
- If you do nothing, your type is linear, meaning that it moves from place to place and can never be copied in any way. (We need a better name for that.)
- If you implement Clone, your type is cloneable, meaning that it moves from place to place, but it can be explicitly cloned. This is suitable for cases where copying is expensive.
- If you implement Copy, your type is copyable, meaning that it is just copied by default without the need for an explicit clone. This is suitable for small bits of data like ints or points.
What is nice about this change is that when a type is defined, the user makes an explicit choice between these three options.
IE, “move semantics” are the default, “copy semantics” can be adopted, or clone for the more complicated data types or for where it is desired for duplication to be explicit.
- Note that
Clone
is asupertrait
ofCopy
, meaning that we have to deriveClone
if we want to deriveCopy
, but can deriveClone
on its own.
struct Movable {
field: i32
}
#[derive(Clone)]
struct OnlyClonable {
field: i32,
}
#[derive(Copy, Clone)]
struct Copyable {
field: i32,
}
Example
Demonstrate how a struct with Copy
obeys “copy semantics” and gets copied implicitly instead of “moved”
- Declare
structs
, with derived traits, and define a basic function for taking ownership
fn check_if_copied<T: Clone>(x: T) -> T {
println!("address: {:p} (from inner owning function)", &x);
x
}
#[derive(Clone)]
struct OnlyClonable {
field: i32,
}
#[derive(Copy, Clone)]
struct Copyable {
field: i32,
}
- Instantiate variables of both
structs
,cl
that hasClone
andcp
that hasCopy
. cl
is moved intocheck_if_copied
and so is no longer live or usable afterward.cp
is not moved intocheck_if_copied
and lives beyond the call ofcheck_if_copied
.
let cl = OnlyClonable{field: 0};
let cp = Copyable{field: 1};
// OnlyClonable type obeys "move semantics"
check_if_copied(cl); // cl gets moved in as it's not copyable
// COMPILER ERROR. Can't do this! As moved into `report_if_copied`!
println!("address: {:p}", &cl);
// Copyable obeys "copy semantics"
check_if_copied(cp); // cp is implicitly copied here!
// Can! as not moved but copied
println!("address: {:p}", &cp);
Demonstrate the same but with mutation
let mut mcp = Copyable{field: 1};
let mcp2 = check_if_copied(mcp); // as mcp was implicitly copied, mcp2 is a new object
mcp.field += 100;
// values will be different, one was mutated and has kept the data from before the mutation
println!("mcp field: {}", mcp.field);
println!("mcp2 field: {}", mcp2.field);
prints …
mcp field: 101
mcp2 field: 1
Application and Limitations
Copy
Copy
is available only on types whose elements also haveCopy
.- Such elements then need to be the numeric types (
i32
,f64
etc),bool
andchar
. So custom types that contain only basic data types.
#[derive(Copy, Clone)]
struct Copyable {
field: i32,
}
- Any field with a non-copyable type such as
String
orVec
cannot be made Copyable
// Copy cannot be derived as `f2: String` does not implement Copy
#[derive(Copy, Clone)]
struct NotCopyable2 {
field: i32,
f2: String
}
- But custom types that have the
Copy
trait as fields work fine, likeCopyable
from above as a field:
#[derive(Copy, Clone)]
struct Copyable2 {
field: i32,
more: Copyable
}
- Beyond this,
Copy
is not overloadable and can’t be implemented in rust (without usingunsafe
presumably). It’s really just a primitive of the language it seems (see source code for the Copy trait).
Clone
- Like
Copy
,Clone
relies on thestruct's
fields also implementingClone
. - A number of standard library types have implemented
Clone
(see list of implementors in the documentation), including the fundamental collections you’ll find in chapter 8 of The Book:String
,Vec
,HashMaps
and alsoarrays
. - Thus the code below, which involves a more complex
Struct
with fields that are aString
,array
andVec
, compiles just fine. - With the
clone()
method,Clonable
is now able to be duplicated allowing the original to be usable after being passed in tocheck_if_copied()
.
#[derive(Clone)]
struct Clonable {
name: String,
values: [f64; 3],
data: Vec<i32>
}
let clonable = Clonable{
name: "Sam".to_string(),
values: [1.0, 2.0, 3.0],
data: vec![111, 222]
};
// clonable is duplicated with the `.clone()` method
check_if_copied(clonable.clone());
// original still usable as it was never moved
println!("Name; {}", clonable.name);
- But, unlike
Copy
, is overloadable, which means you can implementedClone
for your custom types however you want if necessary. - This would be jumping ahead a bit to
Traits
, but we could implementClone
for ourstruct
above ourselves with something like the below:
struct Clonable {
name: String,
values: [f64; 3],
data: Vec<i32>
}
// Just use each field's `.clone()` implementation, much like the default would do
impl Clone for Clonable {
fn clone(&self) -> Self {
Self {
name: self.name.clone(),
values: self.values.clone(),
data: self.data.clone()
}
}
}
- Or we could see how
Clone
is implemented forOption
:
impl<T> Clone for Option<T>
where
T: Clone,
{
fn clone(&self) -> Self {
match self {
Some(x) => Some(x.clone()),
None => None,
}
}
}
- Basically relies on the implementation of
Clone
for the inner value insideSome
. Note thewhere clause
that limits this toOption
variables that contain values that have theClone
trait.
Deep or Shallow Duplication
- In the case of
Copy
, duplication should always be “deep”.- Which isn’t saying much due to the simplicity of the types that can implement
Copy
.
- Which isn’t saying much due to the simplicity of the types that can implement
- In the case of
Clone
… it depends!- As the implementations of
Clone
on the fields of a customstruct
/enum
are relied on, whether the duplication is deep or shallow depends entirely on those implementations. - As stated in the
RFC
quoted above,Clone
is for complex data structures whose duplication is not trivial such that compromises around performance and duplication depth may need to be made. - A clue can be discerned from the signature of the implementation. For
Option
, above, the inner value was restricted to also having implementedClone
, as the value’s.clone()
method was relied on. Therefore,Option
’s is deep. - Similarly, the
Clone
implementation forVec
has the same restriction on its elements:impl<T, A> Clone for Vec<T, A> where T: Clone,
(see source code), indicating that its implementation is at least one level deep.
- As the implementations of
Overall, this seemed a fundamental part of the language, and I found it weird that The Book didn’t address it more specifically. There’s nothing really surprising or difficult here, but the relative roles of Clone
and Copy
, or the “three step ladder” are important to appreciate I think.