this post was submitted on 27 Jun 2024
4 points (100.0% liked)

Learning Rust and Lemmy

231 readers
1 users here now

Welcome

A collaborative space for people to work together on learning Rust, learning about the Lemmy code base, discussing whatever confusions or difficulties we're having in these endeavours, and solving problems, including, hopefully, some contributions back to the Lemmy code base.

Rules TL;DR: Be nice, constructive, and focus on learning and working together on understanding Rust and Lemmy.


Running Projects


Policies and Purposes

  1. This is a place to learn and work together.
  2. Questions and curiosity is welcome and encouraged.
  3. This isn't a technical support community. Those with technical knowledge and experienced aren't obliged to help, though such is very welcome. This is closer to a library of study groups than stackoverflow. Though, forming a repository of useful information would be a good side effect.
  4. This isn't an issue tracker for Lemmy (or Rust) or a place for suggestions. Instead, it's where the nature of an issue, what possible solutions might exist and how they could be or were implemented can be discussed, or, where the means by which a particular suggestion could be implemented is discussed.

See also:

Rules

  1. Lemmy.ml rule 2 applies strongly: "Be respectful, even when disagreeing. Everyone should feel welcome" (see Dessalines's post). This is a constructive space.
  2. Don't demean, intimidate or do anything that isn't constructive and encouraging to anyone trying to learn or understand. People should feel free to ask questions, be curious, and fill their gaps knowledge and understanding.
  3. Posts and comments should be (more or less) within scope (on which see Policies and Purposes above).
  4. See the Lemmy Code of Conduct
  5. Where applicable, rules should be interpreted in light of the Policies and Purposes.

Relevant links and Related Communities


Thumbnail and banner generated by ChatGPT.

founded 9 months ago
MODERATORS
 

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 and enums (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 and Copy, 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 a supertrait of Copy, meaning that we have to derive Clone if we want to derive Copy, but can derive Clone 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 has Clone and cp that has Copy.
  • cl is moved into check_if_copied and so is no longer live or usable afterward.
  • cp is not moved into check_if_copied and lives beyond the call of check_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 have Copy.
  • Such elements then need to be the numeric types (i32, f64 etc), bool and char. 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 or Vec 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, like Copyable 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 using unsafe 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 the struct's fields also implementing Clone.
  • 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 also arrays.
  • Thus the code below, which involves a more complex Struct with fields that are a String, array and Vec, compiles just fine.
  • With the clone() method, Clonable is now able to be duplicated allowing the original to be usable after being passed in to check_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 implemented Clone for your custom types however you want if necessary.
  • This would be jumping ahead a bit to Traits, but we could implement Clone for our struct 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 for Option:
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 inside Some. Note the where clause that limits this to Option variables that contain values that have the Clone 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.
  • In the case of Clone ... it depends!
    • As the implementations of Clone on the fields of a custom struct/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 implemented Clone, as the value's .clone() method was relied on. Therefore, Option's is deep.
    • Similarly, the Clone implementation for Vec 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.

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.

no comments (yet)
sorted by: hot top controversial new old
there doesn't seem to be anything here