Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

7.6 Custom Types and Pattern Matching

Introduction: Building Custom Data Structures

Section titled “Introduction: Building Custom Data Structures”

In systems programming, the ability to define custom data types is fundamental for modelling real-world entities and organising complex information. Just as C/C++ provides struct and class for this purpose, Rust offers struct and enum as tools for creating custom, type-safe data structures. For C/C++ engineers, understanding Rust’s approach to these constructs is crucial, as they integrate seamlessly with Rust’s ownership and borrowing system, contributing to its memory safety. This section introduces Rust’s structs and enums, with a focus on practical examples and comparisons to familiar C/C++ concepts that we covered earlier in the module.

Structs in Rust are custom data types that allow you to group together related pieces of data. They are similar to structs in C and C++ in that they are composite types, but Rust’s structs come with additional features and integrate deeply with the language’s safety guarantees.

Rust provides three kinds of structs: classic structs (with named fields), tuple structs (named tuples), and unit structs (field-less). All are defined using the struct keyword.

Classic Structs (Named Fields)
These are the most common type of struct, analogous to C/C++ structs or classes with public members. Each field within the struct has a name and a type.

For example, in Rust:

// Define a classic struct named User
struct User {
active: bool,
username: String, // String is Rust's growable, owned string type
email: String,
sign_in_count: u64,
}

Has the equivalence in C/C++:

// C++ equivalent using a struct (or use a class with public members)
struct User {
bool active;
std::string username; // std::string is C++'s growable string type
std::string email;
unsigned long long sign_in_count;
};

In C++, struct members are public by default, while class members are private by default. In Rust, struct fields are private by default within their module unless explicitly marked pub.

Tuple Structs (Named Tuples)
Tuple structs are essentially named tuples. They are useful when you want to give a name to a tuple and make it a distinct type, but the individual fields don’t need their own names. Rust Example:

// Define a tuple struct named Color
struct Color(i32, i32, i32); // Represents RGB values
// Define another tuple struct named Point
struct Point3D(i32, i32, i32); // Represents 3D coordinates

C/C++ Comparison: These are somewhat similar to type defining a std::tuple or a simple struct with unnamed members, but Rust’s tuple structs create distinct types.

// C++ equivalent using std::tuple (C++11 onwards)
using Color = std::tuple<int, int, int>;
using Point3D = std::tuple<int, int, int>;
// Or a C-style struct with unnamed fields (less common/idiomatic)
struct Color_C { int r, g, b; }; // Still usually named fields

Unit Structs (Field-less)
Unit structs are structs that have no fields at all. They are useful in situations where you need to implement a trait (we will cover traits in the next section) on some type but don’t need to store any data within the type itself. They are often used as marker types, i.e., when you want to distinguish different states or capabilities at the type level without storing any actual data within the struct itself. For example, you could have a Document that can be Pending or Published. You could use a generic Document<S> where S is the marker type.

struct Pending;
struct Published;
struct Document<State> { … }

There isn’t a direct, idiomatic C++ equivalent. They might be loosely compared to empty classes or structs used as tags.

To use a struct, you create an instance of it by providing concrete values for its fields. The following is a full code example for a Rust struct User (note that each separate instance of User is highlighted in the following example for clarity):

// Define a classic struct named User -- from previous example
struct User {
active: bool,
username: String, // String is Rust's growable, owned string type
email: String,
sign_in_count: u64,
}
fn main() { // 3 different ways to create the struct
// Create an instance of the User struct
let user1 = User {
active: true,
username: String::from("derek123"),
email: String::from("derek@dcu.ie"),
sign_in_count: 1,
};
println!("User1 email: {}", user1.email);
// Field init shorthand (if variable name matches field name)
let email = String::from("joe@example.com");
let username = String::from("joe456");
let user2 = User {
email, // This is the field init shorthand: equivalent to email: email
username, // Equivalent to username: username
active: false,
sign_in_count: 5,
};
println!("User2 email: {}", user2.email);
// Struct update syntax (like C++ copy constructor with modifications)
let user3 = User {
email: String::from("charlie@example.com"),
..user1 // Copy fields from user1. user1 is moved if its fields are not Copy
};
println!("User3 email: {}", user3.email); // Error if user1 was moved
println!("User3 username: {}", user3.username);
}

Note that for user2, email and username are used as shorthand because the local variable names match the struct’s field names. The active and sign_in_count fields are initialized explicitly as their corresponding local variables (if they existed) do not match their field names, or simply because the values are literals.

The code gives the output:

User1 email: derek@dcu.ie
User2 email: joe@example.com
User3 email: charlie@example.com
User3 username: derek123

In Rust, the .. (double-dot) syntax within a struct literal allows you to copy or move the remaining fields from another struct instance to create a new one, without having to explicitly list every field. This syntax is a convenient shorthand for creating slightly modified copies of existing struct instances.

When using struct update syntax (..user1), if user1 contains fields that are not Copy types (like String), user1 will be moved, and you won’t be able to use it afterward. If all fields are Copy types, user1 will be copied instead of moved.

C/C++ Equivalent (Classic Struct):

User user1 = {true, "derek123", "derek@dcu.ie", 1}; // Aggregate initialisation
User user2; // Default construction
user2.active = false;
user2.username = "joe456";
user2.email = "joe@example.com";
user2.sign_in_count = 5;
// C++ copy constructor with modifications (requires custom constructor)
User user3 = user1; // Copy construct
user3.email = "charlie@example.com";

Accessing and Modifying Fields
Fields of a struct instance are accessed using dot notation (.). To modify a field, the struct instance itself must be declared as mutable (let mut).

Rust Example: Using the same User and Color structs from the previous examples:

struct User {
active: bool,
username: String, // String is Rust's growable, owned string type
email: String,
sign_in_count: u64,
}
struct Color(i32, i32, i32); // Represents RGB values
fn main() {
let mut user1 = User {
active: true,
username: String::from("derek123"),
email: String::from("derek@dcu.ie"),
sign_in_count: 1,
};
// Access fields
println!("User email: {}", user1.email); // Output: User email: alice@example.com
// Modify a field (user1 must be mutable)
user1.email = String::from("new_derek@dcu.ie");
println!("New user email: {}", user1.email); // Output: New user email
// Accessing fields of a tuple struct
let my_color = Color(255, 128, 0);
println!("Red: {}, Green: {}, Blue: {}", my_color.0, my_color.1, my_color.2);
}

Gives the output:

User email: derek@dcu.ie
New user email: new_derek@dcu.ie
Red: 255, Green: 128, Blue: 0

C/C++ Equivalent:

User user1 = {true, "derek123", "derek@dcu.ie", 1};
std::cout << "User email: " << user1.email << std::endl;
user1.email = "new_derek@dcu.ie";
std::cout << "New user email: " << user1.email << std::endl;
Concept Match

Match Struct Types and Features

Drag each definition into its matching concept slot, then click Submit. Tap × to return a placed card to the pool.

Classic Struct
drag a definition here…
Tuple Struct
drag a definition here…
Unit Struct
drag a definition here…
..syntax
drag a definition here…
pub
drag a definition here…

Definition Pool

A field-less struct primarily used as a marker type.
The struct update syntax used to copy remaining fields from another instance.
The keyword required to make a struct or its fields visible outside its module.
A struct with named fields, similar to a C++ class with public members.
A named collection of unnamed fields, accessed by index (e.g., .0).
Code Cloze
Rust

Struct Shorthand and Update Syntax

Drag snippets from the pool into the blanks so the program produces the output shown, then click Submit.

1struct User {
2 active: bool,
3 username: String,
4}
5
6fn main() {
7 let username = String::from("derek123");
8 let user1 = User {
9 active: true,
10 ·····, // Field init shorthand
11 };
12
13 let user2 = User {
14 active: false,
15 ·····user1 // Struct update syntax
16 };
17}

Available Snippets

String
name
copy
username
..
...

In Rust, functions associated with a struct (or enum or trait) are defined within an impl (implementation) block. These can be either associated functions (like static methods or constructors in C++) or methods (instance methods in C++).

Rust Example (Full Code)

// Define a Point struct
struct Point {
x: f64,
y: f64,
}
// Implement methods and associated functions for Point
impl Point {
// Associated function (like a static method or constructor)
// Often used as a constructor, by convention named new
fn new(x: f64, y: f64) -> Self { //Self is an alias for the type Point
Point { x, y }
}
// Associated function to create an origin point
fn origin() -> Self {
Point { x: 0.0, y: 0.0 }
}
// Method (takes an immutable reference to self)
// &self is shorthand for self: &Self
fn distance_from_origin(&self) -> f64 {
((self.x * self.x) + (self.y * self.y)).sqrt()
}
// Method (takes a mutable reference to self)
// &mut self is shorthand for self: &mut Self
fn translate(&mut self, dx: f64, dy: f64) {
self.x += dx;
self.y += dy;
}
// Method (take of self)
// self is shorthand for self: Self
fn consume_and_print(self) {
println!("Consuming Point at ({}, {})", self.x, self.y);
// self is moved here, so the original variable cannot be used afterwards
}
}
fn main() {
let p1 = Point::new(3.0, 4.0); // Call associated function
let p_origin = Point::origin();
println!("Distance from origin for p1: {}", p1.distance_from_origin());
// Call method with immutable borrow
let mut p2 = Point::new(1.0, 2.0);
p2.translate(5.0, -1.0); // Call method with mutable borrow
println!("Translated p2: ({}, {})", p2.x, p2.y); // Output: Translated p2: (6, 1)
let p3 = Point::new(10.0, 20.0);
p3.consume_and_print(); // Call method that takes ownership
// println!("p3: ({}, {})", p3.x, p3.y); // ERROR: use of moved value: `p3`
}

This code gives the output:

Distance from origin for p1: 5
Translated p2: (6, 1)
Consuming Point at (10, 20)

This Rust code program demonstrates the powerful language features related to structs that make such object-oriented-like patterns not only possible but safe and efficient. Rust employs the struct keyword to define custom data types, bundling related data into a single unit. Here, our Point struct groups two f64 values, x and y, representing coordinates. This provides clear data encapsulation.

The flexibility emerges from the impl block, which is Rust’s mechanism for implementing associated functions and methods on a type. Think of an impl block as where you define the behaviour that the struct possesses, much like we did when writing classes in C++. There are two types of functions:

  • Firstly, we see Associated Functions. These are functions defined within the impl block that do not take self as their first parameter. They are invoked directly on the type itself, much like static methods or constructors in C++.
  • Secondly, we have Methods. These are functions defined within an impl block that do take self as their first parameter. Rust’s strict ownership system dictates how self is passed, and this is where safety and control is managed.

This is very similar to how we wrote this code in C++.

  • Associated Functions (Point::new(), Point::origin()) are directly analogous to static methods in C++ classes.
  • Methods (&self): These are equivalent to const member functions in C++. They receive an immutable reference to the instance (this pointer in C++ is implicitly const Point_Cpp* const).
  • Methods (&mut self): These are equivalent to non-const member functions in C++. They receive a mutable reference to the instance (this pointer is implicitly Point_Cpp* const).
  • Methods (self): These methods take ownership of the instance, meaning the original variable cannot be used after the call. This is similar to passing an object by value in C++ where the object is copied, or explicitly moving it (std::move) if a move constructor is defined.
Concept Match

Methods and self

Drag each definition into its matching concept slot, then click Submit. Tap × to return a placed card to the pool.

&self
drag a definition here…
&mut self
drag a definition here…
self
drag a definition here…
Associated
drag a definition here…
Self
drag a definition here…

Definition Pool

Takes ownership of the instance, moving it into the method.
Functions in an impl block that don't take self; often used as constructors.
A type alias within an impl block that refers to the type being implemented.
A mutable reference to the instance, allowing field modifications.
An immutable reference to the instance, similar to a const member function in C++.
Code Order
Rust

Order an impl Block

Enums (enumerations) in Rust are powerful custom data types that allow you to define a type by enumerating its possible variants. Unlike simple C/C++ enums, Rust’s enums can have data associated with each variant, making them highly versatile for representing complex, structured choices. This makes them akin to “tagged unions” or “sum types” in other languages.

Enums are defined using the enum keyword, followed by the enum’s name and its variants.

Rust Example (Simple Enum - Unit-like Variants):

// Define a simple enum for traffic light colors
enum TrafficLight {
Red,
Yellow,
Green,
}

This is directly analogous to enum class in C++ (scoped enums).

enum class TrafficLight_Cpp {
Red,
Yellow,
Green
};

A key feature of Rust enums is that each variant can optionally hold data. This data can be structured like a tuple or like a struct.

Tuple-like Variants
These variants hold unnamed data, similar to tuple structs.

// Enum for messages in a game, with tuple-like data
enum Message {
Quit, // No data
Move(i32, i32), // Holds x and y coordinates
Write(String), // Holds a String message
ChangeColor(i32, i32, i32), // Holds RGB values
}

Struct-like Variants
These variants hold named data, similar to classic structs.

// Enum for different shapes, with struct-like data
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { side1: f64, side2: f64, side3: f64 },
}

C++‘s std::variant (C++17 onwards) combined with std::visit provides similar functionality to Rust’s enums with associated data, acting as a type-safe tagged union. Before std::variant, a common pattern was to use a union combined with an enum discriminant, which was less type-safe.

Concept Match

Enum Variants and Data

Drag each definition into its matching concept slot, then click Submit. Tap × to return a placed card to the pool.

Simple Enum
drag a definition here…
Tuple-like
drag a definition here…
Struct-like
drag a definition here…
Tagged Union
drag a definition here…
std::variant
drag a definition here…

Definition Pool

A conceptual name for enums that can hold different types of data.
An enum variant that holds unnamed data fields.
The modern C++ equivalent to Rust's enums with associated data.
An enum variant that holds named data fields.
A list of named variants with no associated data.
Code Cloze
Rust

Defining Enums with Data

Drag snippets from the pool into the blanks so the program produces the output shown, then click Submit.

1enum WebEvent {
2 PageLoad,
3 KeyPress(char),
4 Paste(·····),
5 Click { x: i32, y: i32 },
6}
7
8fn main() {
9 let pressed = WebEvent::KeyPress('x');
10 let pasted = WebEvent::Paste(·····("hello"));
11}

Available Snippets

char
value
String
&str
content
String::from

The match expression is Rust’s primary way to handle enums with associated data. It allows you to execute different code blocks based on the variant of the enum, and it can also destructure the data contained within the variants. A crucial aspect of match is its exhaustiveness: the compiler ensures that all possible variants are handled, preventing unhandled cases that can lead to typical bugs in C++ switch statements.

The full code example is as follows:

enum Message {
Quit, // No data
Move(i32, i32), // Holds x and y coordinates
Write(String), // Holds a String message
ChangeColor(i32, i32, i32), // Holds RGB values
}
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { side1: f64, side2: f64, side3: f64 },
}
fn process_message(msg: Message) {
match msg {
Message::Quit => {
println!("Quitting the application.");
}
Message::Move(x, y) => { // Destructuring tuple-like variant
println!("Moving to coordinates ({}, {}).", x, y);
}
Message::Write(text) => { // Destructuring String variant
println!("Writing message: \"{}\".", text);
}
Message::ChangeColor(r, g, b) => { // Destructuring tuple-like variant
println!("Changing color to RGB({}, {}, {}).", r, g, b);
}
}
}
fn calculate_shape_area(shape: Shape) -> f64 {
match shape {
Shape::Circle { radius } => { // Destructuring struct-like variant
std::f64::consts::PI * radius * radius
}
Shape::Rectangle { width, height } => { // Destructuring struct-like variant
width * height
}
Shape::Triangle { side1, side2, side3 } => { // Destructuring struct-like variant
// Heron's formula for triangle area
let s = (side1 + side2 + side3) / 2.0;
(s * (s - side1) * (s - side2) * (s - side3)).sqrt()
}
}
}
fn main() {
process_message(Message::Move(10, 20));
process_message(Message::Write(String::from("Hello Rust!")));
process_message(Message::Quit);
let circle = Shape::Circle { radius: 5.0 };
println!("Circle area: {}", calculate_shape_area(circle));
let rect = Shape::Rectangle { width: 4.0, height: 6.0 };
println!("Rectangle area: {}", calculate_shape_area(rect));
}

This gives the output:

Moving to coordinates (10, 20).
Writing message: "Hello Rust!".
Quitting the application.
Circle area: 78.53981633974483
Rectangle area: 24

Rust’s structs and enums provide powerful and flexible ways to define custom data types, essential for organising complex programs in systems and edge computing. Structs allow for clear grouping of related data, while enums offer a robust mechanism for representing distinct choices, with the added benefit of carrying associated data.

For C/C++ engineers, the key takeaways are:

  • Rust’s structs are similar to C/C++ structs, emphasising data aggregation. The impl blocks provide a structured way to define associated functions (like static methods) and methods (instance methods), with explicit &self, &mut self, and self to manage borrowing and ownership.
  • Rust’s enums are far more powerful than traditional C/C++ enums, acting as type-safe tagged unions that can hold diverse data for each variant.
  • The match expression, especially when used with enums, is critical to Rust’s safety. Its compile-time exhaustiveness check prevents unhandled cases, a common source of bugs in C++ switch statements.

These custom types are fully integrated with Rust’s ownership and borrowing system, enabling developers to write expressive, memory-safe, and performant code, which is particularly beneficial for resource-constrained and critical applications in edge programming and computer architectures.

Here is a short example for you to test your knowledge of enumerations. There is a small simulation below and underneath that there is a solution. Try not to look at the solution until you have at least attempted the question.

Write a Mobile Robot example that demonstrates an enum for robot movement and pattern matching. The code should define a Command enum with variants for Move, TurnLeft, and TurnRight. The Move variant should include a f64 value representing the distance to move. The Robot struct should store the robot’s position and direction in radians. There should also be an execute_command method that uses match for pattern matching on the Command enum, allowing the robot to respond appropriately to each command.

Give some example code to demonstrate it moving: For example, with the origin on the bottom left of the screen, start at the origin facing right (0.0, 0.0, 0.0), move forward by 5.0, turn left, move forward by 3.0, turn right three times, and move forward by 2.0. Don’t plot the output — it is here for illustrative purposes only.

Rust pattern matching

Robot Movement

Watch a mobile robot execute a sequence of Move / TurnLeft / TurnRight commands — its position and heading update live on the grid.

Robot Path
StartRobot
-10123456-101234XY(0,0)(0.0, 0.0)
Commands
Start
x = 0.0, y = 0.0, direction = 0 rad (→ East)
  1. 1Command::Move(5.0)
  2. 2Command::TurnLeft
  3. 3Command::Move(3.0)
  4. 4Command::TurnRight
  5. 5Command::TurnRight
  6. 6Command::TurnRight
  7. 7Command::Move(2.0)
Speed
Robot statex = 0.0y = 0.0direction = 0° (→ East)
Move(d)
x += d·cos(dir)
y += d·sin(dir)
TurnLeft
dir += PI / 2 (+90°)
TurnRight
dir -= PI / 2 (−90°)
Show Solution

Here is one possible solution that demonstrates the use of enums

use std::f64::consts::FRAC_PI_2; // Represents PI / 2
/// Command enum defines the possible actions for the robot.
pub enum Command {
Move(f64), // Move forward by a given distance
TurnLeft,
TurnRight,
}
/// Robot struct stores the state of our mobile robot.
#[derive(Debug)] // This allows us to print the struct for debugging.
pub struct Robot {
x: f64,
y: f64,
direction: f64, // Direction in radians
}
impl Robot {
/// Creates a new robot at a given position and direction.
pub fn new(x: f64, y: f64, direction: f64) -> Self {
Robot { x, y, direction }
}
/// Executes a command using pattern matching to update the robot's state.
pub fn execute_command(&mut self, command: Command) {
match command {
// If the command is Move, extract the distance and update x and y.
Command::Move(distance) => {
self.x += distance * self.direction.cos();
self.y += distance * self.direction.sin();
}
// If the command is TurnLeft, add 90 degrees (PI/2 radians) to the direction.
Command::TurnLeft => {
self.direction += FRAC_PI_2;
}
// Command is TurnRight, subtract 90 degrees (PI/2 radians) from direction.
Command::TurnRight => {
self.direction -= FRAC_PI_2;
}
}
}
}
fn main() {
// 1. Initialize the robot at the origin (0,0) facing right (0 radians).
let mut robot = Robot::new(0.0, 0.0, 0.0);
println!("Starting position: {:?}", robot);
// 2. Define a sequence of commands for the robot to execute.
// This is a nice way to do it. Calling robot.execute_command() directly is fine.
let commands = vec![
Command::Move(5.0),
Command::TurnLeft,
Command::Move(3.0),
Command::TurnRight,
Command::TurnRight,
Command::TurnRight,
Command::Move(2.0),
];
// 3. Execute each command and print the robot's state.
for (i, command) in commands.into_iter().enumerate() {
robot.execute_command(command);
println!("After command {}: {:?}", i + 1, robot);
}
}

This code gives the following output:

Starting position: Robot { x: 0.0, y: 0.0, direction: 0.0 }
After command 1: Robot { x: 5.0, y: 0.0, direction: 0.0 }
After command 2: Robot { x: 5.0, y: 0.0, direction: 1.5707963267948966 }
After command 3: Robot { x: 5.0, y: 3.0, direction: 1.5707963267948966 }
After command 4: Robot { x: 5.0, y: 3.0, direction: 0.0 }
After command 5: Robot { x: 5.0, y: 3.0, direction: -1.5707963267948966 }
After command 6: Robot { x: 5.0, y: 3.0, direction: -3.141592653589793 }
After command 7: Robot { x: 3.0, y: 2.9999999999999996, direction: -3.141592653589793 }
Code Cloze
Rust

Exhaustive Pattern Matching

Drag snippets from the pool into the blanks so the program produces the output shown, then click Submit.

1enum Status {
2 Ok,
3 Error(u32),
4}
5
6fn check(s: Status) {
7 match s {
8 Status::Ok => println!("All good"),
9 ····· => println!("Something went wrong"),
10 }
11}
Expected Output
Something went wrong

Available Snippets

_
Status::Error
Status::Error(_)
default
Error
Code Cloze
Rust

Method Call Syntax

Code Order
Rust

Build a Complex Enum Match