Rust Bite - Data Operations

bind, copy, borrow, move, clone, mutate

In this bite, we focus on basic data operations:

1. Our goal is to understand the terms:

- Bind: Associate an identifier with a value
- copy: Bind to a copy of a blittable type's value. Compiler-generated code copies bytes from source to destination. Fast.
- borrow: Create a named reference (pointer with special syntax and semantics) to an identifier's location. Borrow pointers must satisfy Rust's ownership rules, covered in an upcoming Bite. Safe code can only dereference borrows.
- Move: Transfer ownership of a type's resources, usually implicitly. The compiler creates a destination pointer to the source's heap resources and invalidates the source instance. Fast.
- Clone: Create a copy of a non-blittable type. Program code invokes it. Slower than move.
- mutate:  Change the value associated with a mutable identifier.

2. Rust Types

A few words about Rust types:
A type defines a set of allowed values and the operations legal for that set.
The Rust language defines a rich set of primitive types:
- bool
- char (utf-8)
- integers: i8, i16, i32, i64, isize, u8, u16, u32, u64, usize
- floats: f32, f64
- aggregates: array: [T;N], slice: [T], str: literal string "....", tuple: (T1, T2, ...), struct { T1, T2, ... }
Assuming T is a primitive type, each occupies a contiguous block of memory whose size depends on the type. A memcpy operation copies them - they are blittable, again provided that T is primitive. Rust uses the term "Copy trait" for blittability. Rust primitives are all Copy types. The Rust Libraries define a large set of non-primitive types:
- String a stack-based object holding a collection of utf-8 chars in the heap
- Vec<T> very like a String, but holding a heap-based collection of an arbitrary type, T
- VecDeque<T> stack-based object holding a heap-based collection of T objects with efficient access to both front and back
- Map<K, V>: an associative container holding key-value pairs in the heap
- ...
Generic type values T, K, and V have a fixed constant size for each type. String holds char instances whose sizes depend on content, varying between 1 and 4 bytes since they are utf-8 values. The types listed above occupy more than one contiguous memory block, so cannot be blitted. Code can move them, but not copy them. Types implementing the Clone trait - all types above do - support explicit copying through the clone operation. These are Move types. User-defined types are either Copy or Move, depending on whether they occupy one contiguous memory block. No type can be both. The Rust Story provides more details and examples: Rust types

3. Binding to a Value

Bind - associate an identifier with a memory location
  • A type is a set of legal values with associated operations.
  • Every identifier has a type:
    let k: i32 = 42;
    let creates a binding. i32 designates a 32-bit integer type. 42 is the value stored at k's memory location.
  • Type inference:
    let k = 42;
    This binding is legal and equivalent to the previous one. Without other information, Rust assigns type i32 to any unadorned integral value that fits a 32-bit location.

4. Binding to an identifier

Binding to an identifier has several forms:
  • let j:i32 = k; // makes copy for j because k is blittable
  • let l = &k;    // l makes a reference to k, called a borrow
  • let s:String = "a string".to_string();
  • let t = s;     // moves s into t, e.g., transfers ownership as s is not blittable
Both sides of a binding expression must share the same type. Rust does not implicitly convert types.
 

5. Assignment

Assignment in Rust, as in many languages, is an expression like:
  • x = y  // copy if x and y are Copy types, y is valid after assignment
  • t = s  // move if s and t are Move types, s is invalid after assignment
Both sides of an assignment expression must share the same type. Assignments usually appear in statements - expressions terminated with a semicolon.

6. Copy and Borrow

Figure 1. CopyType Copy
A borrow is a non-owning reference pointing to an identifier.
  • Copies happen implicitly when an identifier binds to a Copy type:
    let i = 3; let j = i;   // copy
    or when one Copy type is assigned to another:
    j = i + 1;   // copy
  • Borrows arise when binding references to other identifiers:
    let r = &i   // borrow;
    A reference like &i is a pointer to the memory location bound to i. Rust ownership rules govern references and forbid resetting them. A later Bite covers ownership.

7. Copy, Move, and Clone Traits

Figure 2. Str Copy
Figure 3. String Move
Figure 4. String Clone
Traits act like interface contracts. They specify behavior that a type must implement to have the trait. Copy types implement the Copy trait.
  • A type must be blittable to qualify for the Copy trait.
  • The str type represents immutable literal strings. Each lives in static memory for the program's lifetime. Code always accesses them through a reference, e.g., s:&str = "a literal string". The reference gets copied, as Figure 2 shows.
Move types do not implement the Copy trait.
  • Move types are non-blittable, with one exception.
  • Adding the Drop trait creates a Move type, even for blittable types.
  • When execution leaves a scope, all move types declared in that scope are dropped, returning their resources through Drop::drop(). This resembles a C++ destructor invocation.
Clone types implement the Clone trait.
  • Types with the Clone trait provide a clone() member function. It creates a new instance with the same structure and copies of any resources held by the cloner.
  • Examples of Clone types are the collections, e.g., Strings, Vecs, VecDeques, Maps, ...

8. Move and Clone

  • A move transfers a Move type's heap resources to another instance.
    • Figure 3 shows String s moved to t with the statement:
      • let t = s;   // s is now invalid
    • Move transfers ownership of resources.
  • A clone copies a Move type's heap resources into a new instance.
    • Figure 4 shows String s cloned with the statement:
      • let t = s.clone();   // s is still valid
    • The clone operation copies resources to the target.

9. Mutation

Rust data is immutable by default - code cannot change it. Code opts in to mutation with the mut qualifier.
  • Immutable data:
    let i = 1;
    // i += 1; won't compile
  • Mutable data:
    let mut j = 1;
    j += 1; // compiles since j is mutable
Data mutability plays a central role in Rust's ownership policies, which ensure memory safety.

10. Traits Preview:

A trait specifies one or more function signatures that a type with the trait must implement. Marker traits like Copy are an exception, declaring no signature but still affecting compiler-generated code. Traits constrain generic parameter types and support dynamic dispatching in polymorphic designs. The standard library defines std::marker::Copy and std::clone::Clone. Many more exist, and later Bites will introduce some. Move is not among them. I include Move in this table because it acts like a trait in some ways. Data moves during construction or assignment if and only if it is not Copy. I think of Move as a trait, even though it cannot constrain generic types (more on that in the Functs Bite).
Trait Applies to: Examples Consequences
Copy Single contiguous memory block
==> blittable
ints, floats, aggregates of Copy types Copies value from one memory location to another. Source valid after copy
"Move" non-contiguous block
==> not blittable
Strings, Vecs, VecDeques, ...
stack-based aggregates managing instances in the heap
Transfers data ownership to another identifier.
Source invalid after move
Using a "Moved" variable causes a compile error.
Clone most types Structs, Strings, Vecs, VecDeques, ... Copies resources to another identifier. Source valid after clone

11. Formatting Data

Much of the data Rust code generates ends up formatted for display or for building strings. The print! and format! macros handle this, taking format specifications like: Format Specifiers: let arg1 = "abc"; let arg2 = 123 let s: String = format!("\n {:?} and {}", arg1, arg2); The last line uses the format! macro. The "{xx}" tokens are placeholders for format specifications. The first, {:?}, formats arg1 with the debug specifier - a simple format that most Rust library and user-defined types support. The second, {}, holds no specifier, requesting a custom display format. The std::fmt crate documents many predefined specifiers and options. Remaining Bites use these as needed without further clarification.

12. Conclusions:

Move, Copy, Clone, Borrow, and Drop are central to the Rust memory model and its guarantees about memory and data race safety. The ownership Bite covers more. Binding and mutation form Rust's interface to data values. Combining these features supports a scope-based value model for data - without the complexity of multiple constructors (copy, move, conversion), user-defined assignment operators, and destructors.

13. Exercises:

Note:
To build and run with cargo from the Visual Studio Code terminal, open VS Code in the package folder - the folder containing the package's cargo.toml file.
  1. Create an instance of a blittable type and show when it is copied.
    • Can you prove that it was copied?
  2. Create an instance of a non-blittable type and show when it is moved.
    • Can you prove that it was moved?
    • Can you show that the moved-from is invalid?
  3. Repeat the second exercise but clone the non-blittable type instead of moving it.
    • Can you show that the cloner is still valid?

14. Solution for Exercise #1

Solution 0.5rem
  Addresses are different, values are the same => copy.   voila!
Lines 5 and 6 answer the main question. The remaining lines address the "Can you prove ..." addendum.