about
Bits Data Rust
11/25/2023
0
Bits Repo Code Bits Repo Docs

Bits:  Rust Data Types

types, initialization, ownership, borrowing

This page guides you through the rust type system in several sections: The menu bar at the right has links to each of these, so they are always in view anywhere in this page. The bottom menu bar "Sections" item has the same information in a drop-up list. The small persistant fixed menu on the right switches between languages for the same topic. To simplify code comparisons these pages open to their last scroll positions. Most other pages open at the top.

Synopsis:

This page demonstrates simple uses of the most important Rust types. The purpose is to quickly acquire some familiarity with types and their uses.
  • Rust provides two categories of types: copy and move. Copy types occupy a single contiguous block of stack memory. Move types have control blocks in stack memory used to manage resources in the heap.
  • Primitive types and aggregates of primitive types are copy. Construction, assignment, and pass-by-value copies the source's value to the destination.
  • All other types are move. Construction, assignment, and pass-by-value move ownership of the value's heap resources to the destination. This makes the source invalid. Attempting to use a "moved" variable is a compile error.
  • The source and destination of copy and move operations must have exactly the same type.
  • More details about copy and move operations can be found in Rust Bite - Data Operatons, including diagrams and code examples.
  • Most move types provide clone operations that support making copies, but using code must explicitly call clone().
  • Rust supports making fixed references to either copy or move types. These may refer to instances in a function's stack memory, in the native heap, or in the program's static memory.
  • Rust references are constrained by Rust ownership rules to support memory and data race safety by construction. All values in memory have a single owner, responsible for its creation, access, and deallocation.
  • Ownership can be borrowed by creating a reference. Program code may create an arbitrary number of non-mutable references to some variable, but may only create a single mutable reference that borrows exclusive access to the variable.
  • Rust's type system plays a dominant role in providing memory and data race safety.
  • Here we begin to see significant differences between the languages, especially when comparing statically typed languages like C++, Rust, and C#, with dynamically typed languages like Python and JavaScript.
Rust Types Details 

Table 1.0 Rust Types

Type Comments Example
-- Integral types ----
bool values true and false let b = true;
i8, i16, i32, i64, isize, u8, u16, u32, u64, usize signed and unsigned integer types let i = 42i8;
-- Floating point types ----
f32, f64 values have finite precision, and may have approximate values let f:f32 = 3.14159;
-- literal string types --
&str A literal string let ls = "literal string";
let second = ls.chars().nth(1);
&str Slice of a literal string let slice = &ls[1..5];
-- Aggregate types ----
[T; N] An array of N elements all of type T let arr = [1, 2, 3, 2, 1];
let first = arr[0];
&[T] Slice of array of elements of type T let arrs = &arr[1..3];
let second = arrs[1];
Tupl collection of heterogeneous types accessed by position let tu = (42i32, 3.14159f64, 'z');
let third = tu.2;
Result<T,E> Result enum holds result Ok(t:T) or Err(e:E) fn doOp1(..args) -> Result<r:R, e:Err>
Option<T> Option enum holds optional value Some(t:T) or None fn doOp2(..args) -> Option<T>
-- Std::library types ----
String Expandable collection of utf-8 characters allocated in the heap let strg = "a utf-8 String".to_string();
Vec<T> Expandable generic collection of items of type T let v = Vec::<f64>::new(), v.push(1.5); ...
VecDeque<T> Expandable double-ended generic collection of items of type T let vd = VecDeque::<f64>::new(),
v.push_front(1.5); ...
HashMap<K,V> Associative container of Key-Value pairs, held in a table of bucket lists. let map = HashMap::<&str, int>::new(),
map.insert("zero", 0); ...
LinkedList, BTreeMap, HashSet, BTreeSet, BinaryHeap, ... The Rust std::library defines types for threading and synchronization,
reading and writing to streams, anonymous functions, ...
Crate std
-- User-defined Types --
User-defined types Based on structs and enums, these will be discussed in the next Bit.
Rust Type System Details 

Table 2. Rust Copy and Move Operations

Operation Example Copy type Move type
Construction let t:T = u:T u's value is copied to t,
u is still valid
u's value is moved to t,
u now invalid
Assignment t:T = u:T u's value is copied to t,
u is still valid
u's value is moved to t,
u now invalid
Pass-by-value fn doOp(t:T) t's value is copied to doOp stack frame,
t is still valid
t's value is moved to doOp stack frame,
t is now invalid

Table 3. Rust Copy and Move Types

Property Members Notes
Primitive types integers, floats, literal strings These are all copy types
Aggregate types arrays, slices of arrays, tuples, Result<T,E>, Option<T> These are copy types if their members are copy, otherwise they are move types
std::library collection types String, Vec<T>, VecDeque<T>, HashMap<K,V>, ... These are all move types

Table 4. Rust Type System Attributes

Static typing All types are known at compile time and are fixed throughout program execution.
Inference Compiler infers types in expressions if not explicitly annotated. Occasionally inference fails and explicit annotation is required.
Strong typing Types are exhaustively checked and there are very few implicit conversions.
  • Numeric and boolean literals coerce to their correspoinding type, e.g., 42 to i32.
  • Variables without type annotations are coerced to their minimal types
  • Values can be coerced to a more specific type of same or larger size.
  • Smart pointers like Box, Rc, Arc, ... implement the DeRef trait, supplying a method deref() that returns a reference to their inner values. Applying * to these types automatically calls deref() to return an inner reference.
Algebraic data types Types created using enums and structs. Unlike other languages, Rust enums can hold named discriminants with associated data of arbitrary type. That combined with Rust's matching operations simplify state and error handling.
Examples: Result<T,E> { Ok(T), Err(E), }, Option<T> { Some(T), None, }
Matching:
    match result {
       Ok(value) => { // do something with value },
       Err(error) => { // do something with error }
    }
Generics Generics provide types and functions with unspecified parameters, supporting code reuse and abstraction over types. Generic parameters are specified at the call site, often bound by constraints, e.g., fn doOp<T:Debug, Clone>(t:T).
The function doOp compiles only if T satisfies its named trait bounds Debug supporting use of format {:?} and Clone supporting explicit copy with clone().
Traits Traits are similar to Java and C# interfaces. They defined shared behavior that types can implement, supporting abstraction over behavior. Traits define behavior by declaring trait specific functions. A trait may, but need not, define its function's contents. Generics and Traits are covered here.

1.0 Initialization

Rust types are initialized with a let statement that binds a variable name to a memory location with specified value.

1.1 Initializing language-defined types

The Rust language defines scalar types: booleans, integers, and floats, and aggregate types: literal strings and slices, arrays and array slices, tuples, and enums Result<T,E> and Option<T>. All of the type variations, e.g., i8, i16, ... are are enumerated in the "Rust Types Details" dropdown, above. The code block below shows how to initialize them and how to access members of an aggregate.
  /*-- initialize language defined types --*/

  let i = 42i8;         // data type specified

  let b = true;         // data type inferred

  let f:f32 = 3.14159;  // variable type specified  

  let c:char = 'z';

  let sl:&str = "a literal string";
  let second = sl.chars().nth(1);

  let st = "an owned string".to_string();
  let fourth = st.chars().nth(3);

  let arr:[i32; 3] = [1, 2, 3];
  let first = arr[0];

  let tp: (i32, f64, char) = (1, 2.5, 'a');
  let second = tp.1;

  let iref: &i8 = &i;

  let aref: &[i32; 3] = &arr;
Type specifications like i8 and f64 can be applied to the variable name or to it's initializing data, as shown here. Rust can usually infer appropriate types for data, but a code developer can explicitly define them. If the type annotations were removed in this example, Rust would infer i to be i32 and f to be f64. The char type occupies 4 bytes of storage, large enough to hold any of the utf-8 characters. The type &str is a reference to a fixed-size immutable literal string in stack memory. It may hold any of the utf-8 characters. It's size is the sum of the actual literal character sizes. For the literal string shown, all of the characters are 1 byte. Arrays are fixed size sequences of values of the specified type. They are indexable and individual element values may be changed if the array has mutable type. Tuples are sequences of values of heterogeneous types. Elements are accessed via position arguments, e.g., tp.1 accesses the second element, a float. References like &arr are fixed pointers to the beginning of the variable's data storage.
Output
  -------------------------
    create and initialize
  -------------------------

  --- initialize language-defined types ---  

  --- i8 ---
  i, i8
  value: 42, size: 1

  --- bool ---
  b, bool
  value: true, size: 1

  --- f32 ---
  f, f32
  value: 3.15927, size: 4

  --- char ---
  c, char
  value: 'z', size: 4

  --- &str ---
  ls, &str
  value: "literal string", size: 16
  second, core::option::Option<char>
  value: Some('i'), size: 4

  --- alloc::string::String ---
  st, alloc::string::String
  value: "an owned string", size: 24
  fourth, core::option::Option<char>
  value: Some('o'), size: 4

  --- [i32; 3] ---
  arr, [i32; 3]
  value: [1, 2, 3], size: 12
  first, i32
  value: 1, size: 4

  --- (i32, f64, char) ---
  tp, (i32, f64, char)
  value: (1, 2.5, 'a'), size: 16
  second, f64
  value: 2.5, size: 8

  --- &i8 ---
  iref, &i8
  value: 42, size: 8

  --- &[i32; 3] ---
  aref, &[i32; 3]
  value: [1, 2, 3], size: 8
  second, i32
  value: 2, size: 4

  --- &str ---
  lscs, &str
  value: "iter", size: 16
  second, core::option::Option<char>
  value: Some('t'), size: 4

  --- &[i32] ---
  sla, &[i32]
  value: [2, 3], size: 16
  second, i32
  value: 3, size: 4
              
Each type in the code block on the left is characterized by its value, its type evaluated using std::any::type_name::<T>() and its size retrieved using std::mem::size_of::<T>(). The function showType formats that information for display and is defined in the bits_data_analysis.rs file, shown in Section 1.5, below.
Scalar types are held in a single block of contiguous heap memory. Copies of all scalar types are byte-wise copies of the source data. That results in two unique instances residing in the invoking function's stackframe.
Language-defined aggregate types with scalar members are also held in a contiguous region of stack memory, and are copied with byte-wise copy. Aggregates with members using heap allocations like strings and vectors are moved instead of copied.
Copies, moves, and clones are discussed in section 1.4, below.
The char type is large enough to hold any utf-8 character, e.g., 4 bytes. literal strings and instances of the string type are sequences of utf-8 characters. Each utf-8 character consits of from 1 to 4 bytes. That supports many different character sets , ASCII, Unicode, Mandarin, Kanji, Arabic, ... Rust strings pack the utf-8 characters so string elements are not fixed size. That means that they are not indexable. Instead, rust provides the chars iterator which returns an Option enumeration holding either Some(ch) or None when the iteration has reached the end of the characer sequence. The character value, ch, is accessed by (conditionally) unwrapping Option. Arrays are fixed size, indexable, collections of values of a single type, defined by the array's declaration. Values are accessed and mutable arrays are modified by index. Tuples are fixed size collections of values of heterogeneous types. Elements are accessed by position: 0, 1, ...

1.11 Specifying locations

Rust supports storing data in static memory for constants and static data, in stack memory for function arguments and data local to a function, and in the native heap. These are illustrated in the code block below.
  /*-- static, stack, and heap allocations --*/

  static PI:f64 = 3.1415927;      // static memory  
  // static address: &PI = 0x7ff730eb7668
  ------------------------------------
  let f:f64 = 3.1415927;          // stack memory
  // stack address:   &f = 0x40a1cff8c0
  ------------------------------------
  let s:&str = "3.1415927";       // stack memory
  // stack address: &str = 0x40a1cff910
  ------------------------------------
  let g = box::new(3.1415927f64); // heap memory
  // heap address:   &*g = 0x1b78cc05bd0

            
Variables can be bound to locations in static memory, stack memory, or heap memory. Static memory is allocated at compile-time and so contents live for the duration of the program. Stack memory is implicitly allocated at run-time for each function call, including main. That serves as scratch-pad storage for input parameters and locally declared data. It is implicitly deallocated when the thread of execution leaves it's function's scope. Heap memory is explicitly allocated with construction of a smart Box pointer. That allocation is returned when the smart pointer, g, goes out of scope.

1.2 Initializing standard library types

  /*-- standard library types --*/

  let v:Vec<i32> = vec![1, 2, 3, 2, 1];
  // size: 24 bytes, control block holds
  // ptr to ints on heap, length, capacity
  ------------------------------------
  let st:String = "an owned string".to_string();
  // size: 24 bytes, control block holds
  // ptr to chars on heap, length, capacity
  ------------------------------------
  let mut vecdeq = VecDeque::<f64>::new();
  vecdeq.push_front(1.0);
  vecdeq.push_front(1.5);
  vecdeq.push_front(2.0);
  vecdeq.push_front(1.5);
  vecdeq.push_front(1.0);
  ------------------------------------
  let mut map = HashMap::<&str,i32>::new();
  map.insert("zero", 0);
  map.insert("one", 1);
  map.insert("two", 2);
            
Standard Library types are initialized with a call to new and possibly modified with subsequent additions of data. Here, vec! is a std::library macro that initializes variable v by creating a Vec<int> and pushing the elements 1, 2, ... Vecs consist of a control block in stack memory that holds a pointer to a resizeable array of elements in the heap, a count of the number of elements currently stored, and a count of the available capacity for data. When the length equals the Vec's capacity and a new element is pushed the Vec allocates a new heap array of twice the size of the current capacity, copies the elements to the new storage and deallocates the original heap array. The string, st, is initialized by converting a literal string to a String type. Strings are sequences of utf-8 characters, each of which may contain from one to four bytes. That supports representing characters from non-roman languages, e.g., arabic, kanji, etc., as well as emoji's and symbols. The structure of a String is very similar to a Vec. It has a control block in stack memory and a collection of characters in the heap. The main difference is that vectors hold elements of a fixed size, and so are indexable. Strings hold utf-8 characters which vary in size from one to four bytes. They are not indexable, but provide an iterator, chars that steps through the character collection. The statement let mut vecdeq = ... creates a double ended VecDeque queue. It must be declared mut to allow later modifications with push-front statements. Data is stored in a circular buffer in the heap, supporting efficiently pushing data onto either front or back of the queue. HashMap is an associative container with, in this example, Key:&str, and Value:i32. The &str type is a reference to a literal string like "zero". Key-value pairs are stored in a table of buckets which are linked lists. Lookup applies a hash function to a unique key to find the table address of the bucket holding that key. The HashMap retrieval API provides methods that use the hash to walk the bucket list searching for the specified key. The code block, below, displays some of the output of a demonstration program in Bits/Rust/rust_data, stored in a Bits repository for all the code used in the Bits for C++, Rust, C#, Python, and JavaScript. You are invited to clone or download the repository and explore these examples using Visual Studio Code.
Output
--- initialize std::lib types ---

vec, alloc::vec::Vec
value: [1, 2, 3, 2, 1], size: 24

st, alloc::string::String
value: "an owned string", size: 24

vecdeq, alloc::collections::vec_deque::VecDeque
value: [1.0, 1.5, 2.0, 1.5, 1.0], size: 32

map, std::collections::hash::map::HashMap<&str, i32>
value: {"one": 1, "zero": 0, "two": 2}, size: 48

              
Each type in the code block on the left is characterized by its value, its type evaluated using std::any::type_name::<T>() and its size retrieved using std::mem::size_of::<T>(). The function showType formats that information for display and is defined in the bits_data_analysis.rs file, shown in Section 1.5, below.
Language-defined aggregate types with scalar members are held in a contiguous region of stack memory, and are copied with byte-wise copy. Aggregates with members using heap allocations like strings and vectors are moved instead of copied. Copies, moves, and clones are discussed in section 1.4, below.

2.0 Safety and Ownership

The design of the Rust programming language and especially its type system focuses on memory and data race safety. That means:
  • Read and write operations happen only within program allocated memory.
  • References are guaranteed to refer to valid targets.
  • Threads may share data only within the confines of a lock. Each thread must acquire a lock to access data and then release it to enable other threads acess.
These properties are guaranteed by construction. The compiler will not build code that violates them.
Rust Bites - Safety has more details about memory and data race safety.

2.1 Single Ownership Policy

To support safety Rust has a "single ownership" policy. Each value in memory has a single owner that manages its allocation, initialization, deallocation and lifetime. this means that:
  • Binding a name to a value assigns ownership of the value to the named variable. Only the owner has the authority to modify, use, or deallocate the value. No other variable can access or modify the value without borrowing the priviledge to do so from the owner.
  • A collection like Vec<T> owns its members. In the code segment below, v owns the vector, and the vector owns the elements [1, 2, 3]. Program code can access them by taking a reference:
      let v = vec![1, 2, 3];
      let rv = &v3[1];
    

2.2 Borrows - Rust References

  • References are used to borrow access to a variable from its owner, e.g., let rv = &v. Rust enforces strict rules for the use of references in order to guarantee memory and thread safety.
    • Non-mutable references can share read-only access to a value. Multiple shared references to a single owned value are valid.
    • Mutable references, often called exclusive references, provide exclusive access to the value. No other references can access the value until the end of the mutable reference's lifetime. That usually occurs when the mutable reference goes out of scope.
    • These rules apply to use, not declaration.
  • This is a strong constraint that may occasionally affect the way programs are designed. No shared mutability ensures that references to a collection, like Vec<T>, don't dangle when the collection reallocates its resource memory to provide more capacity.

    Example: Attempt to mutate collection with active reference

      show_op("attempt to mutate Vec while immutable ref exists");
      let mut v3 = vec![1, 2, 3];
      let rv3 = &v3[1]; // ok to declare
      v3.push(4);  // v3 will reallocate if capacity is 3
      println!("  rv3: {rv3:?}"); // not ok to use
    

    Example: Compiler error message

    C:\github\JimFawcett\Bits\Rust\rust_data
    > cargo run
       Compiling rust_data v0.1.0 (C:\github\JimFawcett\Bits\Rust\rust_data)
    error[E0502]: cannot borrow `v3` as mutable because it is also borrowed as immutable
       --> src\main.rs:417:3
        |
    416 |   let rv3 = &v3[1]; // ok to declare
        |              -- immutable borrow occurs here
    417 |   v3.push(4);  // v3 will reallocate if capacity is 3
        |   ^^^^^^^^^^ mutable borrow occurs here
    418 |   println!("  rv3: {rv3:?}"); // not ok to use
        |                     ------- immutable borrow later used here    
    
    For more information about this error, try `rustc --explain E0502`.   
    error: could not compile `rust_data` (bin "rust_data") due to previous error
    

2.3 Copy, Move, and Clone Operations

  • Copy of primitives and Move are efficient because they copy only a few bytes. Clone is expensive for a collection as it copies both the collection control block and all its heap resources.
  • Some Rust types implement the Copy trait. All primitives and aggregations of primitives are "copy" types. All other types are "move".
  • For all copy types, construction, assignment, and pass-by-value results in data copy with no transfer of ownership, so the source is still valid.
    The str type represents a constant literal string in static memory. All str's are accessed by a reference, &str. The reference &str is copy. So copying an &str results in a second reference pointing to the original address. Figure 1. Str Copy

    Example: &str copy

      let lstrs = "literal string";  // copy type
      let lstrd = lstrs;
      println!("  source: {lstrs:?}, address: {:p}", lstrs);
      println!("  destin: {lstrd:?}, address: {:p}", lstrd);
      nl();
      println!("  Note: a literal string is a fixed value in memory.
      All access occurs through a reference, so copies just copy
      the reference. Both variables point to the same address." 
      );

    Example: Results of &str copy

    --- direct copy &str ---
      source: "literal string", address: 0x7ff7e742b590
      destin: "literal string", address: 0x7ff7e742b590
    
      Note: a literal string is a fixed value in memory.
      All access occurs through a reference, so copies just copy
      the reference. Both variables point to the same address.
    
  • For all move types ownership will be transferred to another variable by construction, assignment, or pass-by-value as a function argument. A "moved-from" variable is invalid. Any use of that will result in a compile error.
    Move is a very efficient operation, copying only a few bites instead of the entire object. When a copy is needed, the developer can choose to explicitly clone the original value. Figure 2. String Move

    Example: String Move

    let s = String::from("a string");
    let addrvec = &s;
    let addrzero = std::ptr::addr_of!(s.as_bytes()[0]);
    println!("address of s: {:p}", addrvec);
    println!("address of first byte of s's char buffer: {:p}\n", addrzero);
    
    let t = s;  // move
    let addrvec = &t;
    let addrzero = std::ptr::addr_of!(t.as_bytes()[0]);
    println!("address of t: {:p}", addrvec);
    println!("address of first byte of t's char buffer: {:p}\n", addrzero);

    Example: Results of String Move

    --- demo_move for String ---
    
    --- let s = String::from("a string") ---
    address of s: 0x25758ff558
    address of first byte of s's char buffer: 0x1725ee5dcd0
    
    --- let t:String = s; // move ---
    address of t: 0x25758ff600
    address of first byte of t's char buffer: 0x1725ee5dcd0
    
    Note: s and t are unique objects
    that share same buffer
    but now, s is invalid
    
  • Move types usually implement the Clone trait with the function clone(). Calling clone makes an independent copy of the source. The original and clone are independent entities with different owners. They have the same values immediately following the clone operation, but may mutate to different values, independently. Figure 3. String Clone

    Example: String Clone

    let s_src = String::from("a string");
    let s_src_addr = &s_src;
    let s_src_bufaddr = std::ptr::addr_of!(s_src.as_bytes()[0]);
    println!("  s_src: {:?}, address: {:p}", s_src, s_src_addr);
    println!("  s_src_bufaddr: {:p}", s_src_bufaddr);
    let s_cln = s_src.clone();
    let s_cln_addr = &s_cln;
    let s_cln_bufaddr = std::ptr::addr_of!(s_cln.as_bytes()[0]);
    println!("  s_cln: {:?}, address: {:p}", s_cln, s_cln_addr);
    println!("  s_cln_bufaddr: {:p}", s_cln_bufaddr);
    nl();
    println!("  Note: s_src and s_cln have different addresses
    and their buffers have different addresses.
    So they are unique entities.");
    

    Example: Results of String Clone

    --- string clone ---
    s_src: "a string", address: 0x7610d0f390
    s_src_bufaddr: 0x229473682f0
    s_cln: "a string", address: 0x7610d0f448
    s_cln_bufaddr: 0x22947375d00
    
    Note: s_src and s_cln have different addresses
    and their buffers have different addresses.
    So they are unique entities.
    

2.4 Indexing

The array, array slice, and all of the std::library sequential collections support indexing. Rust tracks index operations in real time and, if an out-of-bounds index is used, the current thread of execution will "panic" causing an orderly thread termination before any reads or writes are executed. This prevents a memory access vulnerability common to other languages.

2.5 Analysis and Display Functions

When you look at any of the "Output" details, you will see some output with detailed formatting, but you won't see code providing that output in corresponding code sections. Code responsible for formatting and supplying low-level details, like type information, has been elided from the code shown above. The elided code consists of calls to functions shown in the dropdown below. These functions use language features, like generics, that will be covered in later Bits. You can find the complete code, including all the elisions, in the Bits Repository.
Analysis & Display Function Code 
  #![allow(unused_mut)]
  #![allow(dead_code)]
  #![allow(clippy::approx_constant)]

  /* rust_data::bits_data_analysis.rs */
  /*-----------------------------------------------
    Note:
    Find all Bits code, including this in
    https://github.com/JimFawcett/Bits
    You can clone the repo from this link.
  -----------------------------------------------*/

  use std::fmt::Debug;

  /*-- show_type --------------------------------------
  Shows compiler recognized type and data value
  */
  pub fn show_type<T: Debug>(t: &T, nm: &str) {
    let typename = std::any::type_name::<T>();
    print!("  {nm}, {typename}");
    println!(
      "\n  value: {:?}, size: {}",
        // smart formatting {:?}
      t, std::mem::size_of::<T>()   
        // handles both scalars and collections
    );
  }
  /*---------------------------------------------------
    show string wrapped with long dotted lines above 
    and below
  */
  pub fn show_label(note: &str, n:usize) {
    let mut line = String::new();
    for _i in 0..n {
      line.push('-');
    }
    print!("\n{line}\n");
    print!("  {note}");
    print!("\n{line}\n");
  }
  pub fn show_label_def(note:&str) {
    show_label(note, 50);
  }
  /*---------------------------------------------------
    show string wrapped with dotted lines above 
    and below
  */
  pub fn show_note(note: &str) {
    print!("\n-------------------------\n");
    print!(" {note}");
    print!("\n-------------------------\n");
  }
  /*---------------------------------------------------
    show string wrapped in short lines
  */
  pub fn show_op(opt: &str) {
    println!("--- {opt} ---");
  }
  /*---------------------------------------------------
    print newline
  */
  pub fn nl() {
    println!();
  }
/*---------------------------------------------------
  Show initialization of Rust's types
*/
fn show_formatted<T:Debug>(t:&T, nm:&str) {
  show_op(std::any::type_name::<T>());
  show_type(t, nm);
}
            
The functions show_type and show_formatted are generic functions that accept any type T that implements the Debug trait. We will cover these in GenericRust. For now, it is not important to know generics to understand what they do.
show_type displays the calling name via parameter nm and the type name of t ε T. It also shows the value and size of t. The print format {:?} is very useful, as all of the language and library types provide formatted views of their values. We will show in the next Objects Bit how user-defined types can do that as well. The function std::mem::size_of::<T>() measures the size of that part of t that resides in the calling function's stackframe. It does not evaluate the size of data members residing in the heap.
show_label displays a block of text preceded and succeeded by demarkation lines of specified size.
show_note does the same thing with fixed length lines and additional spaces.
show_op displays a line of text with prefix and and suffix short dotted lines.
nl is a shorthand for "printlin!().
show_formatted combines show_op and show_type to display formatted information about a Rust variable t ε T.

3.0 Epilogue

Most of the operations for language-defined and std::library types dicussed in this page will be covered again for user-defined types in Bits_ObjectsRust.

4.0 References

Link Comments
Crate std Rust documentation about primitive and library defined types.
fat pointers Pointer to slices or trait objects contain an address and a length for these dynamically sized types.
Rust Type System #1 Ownership, aliasing, lifetime
Rust Type System #2 Algebraic types, generic associated types
Rust Type System #3 Generic container types, interior mutability