Rust for C++ Programmers: Learn how to embed Rust in C/C++ with ease (English Edition)
()
About this ebook
“Rust for C++ Programmers” is the perfect guide to help you master the Rust programming language. Beginning with its evolution and comparison to C/C++, the book will help you learn how to install and use the powerful Cargo package manager. The book then covers key topics such as bindings and mutability, ownership, conditionals, loops, functions, structs and enums, and more. The book also explains how to handle errors in Rust. Furthermore, the book explores advanced topics such as smart pointers, concurrency, and even building a desktop application using GTK.
By the end of the book, you will be able to build powerful and resilient apps with Rust.
Related to Rust for C++ Programmers
Related ebooks
Rust Mini Reference: A Hitchhiker's Guide to the Modern Programming Languages, #5 Rating: 0 out of 5 stars0 ratingsBeginning Rust Programming Rating: 0 out of 5 stars0 ratingsGolang for Jobseekers: Unleash the power of Go programming for career advancement (English Edition) Rating: 0 out of 5 stars0 ratingsRust In Practice Rating: 0 out of 5 stars0 ratingsIntroduction to Google's Go Programming Language: GoLang Rating: 0 out of 5 stars0 ratingsLearn Rust Programming: Safe Code, Supports Low Level and Embedded Systems Programming with a Strong Ecosystem (English Edition) Rating: 0 out of 5 stars0 ratingsThe C++ Workshop: Learn to write clean, maintainable code in C++ and advance your career in software engineering Rating: 0 out of 5 stars0 ratingsMastering Rust: The Ultimate Starter Guide Rating: 0 out of 5 stars0 ratingsThe Art of Code: Exploring the World of Programming Languages Rating: 0 out of 5 stars0 ratingsC++17 STL Cookbook Rating: 3 out of 5 stars3/5Learning Go Programming Rating: 5 out of 5 stars5/5Go Cookbook Rating: 5 out of 5 stars5/5Learning Go Programming: Build ScalableNext-Gen Web Application using Golang (English Edition) Rating: 0 out of 5 stars0 ratingsPractical Rust 1.x Cookbook Rating: 0 out of 5 stars0 ratingsC# Programming & Software Development: 6 In 1 Coding Syntax, Expressions, Interfaces, Generics And App Debugging Rating: 0 out of 5 stars0 ratingsMastering Rust Programming: From Foundations to Future Rating: 0 out of 5 stars0 ratingsModern C++ Programming: Including the recent standards C++11, C++17, C++20, C++23 Rating: 0 out of 5 stars0 ratingsProgramming Language Concepts: Improving your Software Development Skills Rating: 0 out of 5 stars0 ratingsRust for Network Programming and Automation Rating: 0 out of 5 stars0 ratingsLearning Rust Rating: 0 out of 5 stars0 ratingsLinux System Programming: From Basics to Expert Proficiency Rating: 0 out of 5 stars0 ratingsAtomic Kotlin Rating: 0 out of 5 stars0 ratingsImplementing SSL / TLS Using Cryptography and PKI Rating: 0 out of 5 stars0 ratingsBuilding Server-side and Microservices with Go: Building Modern Backends and Microservices Using Go, Docker and Kubernetes Rating: 0 out of 5 stars0 ratingsBuild Serverless Apps on Kubernetes with Knative: Build, deploy, and manage serverless applications on Kubernetes (English Edition) Rating: 0 out of 5 stars0 ratingsEmbedded Systems Programming with C++: Real-World Techniques Rating: 0 out of 5 stars0 ratingsClojure Web Development Essentials Rating: 0 out of 5 stars0 ratingsMastering Akka Rating: 0 out of 5 stars0 ratings
Computers For You
The ChatGPT Millionaire Handbook: Make Money Online With the Power of AI Technology Rating: 4 out of 5 stars4/5SQL QuickStart Guide: The Simplified Beginner's Guide to Managing, Analyzing, and Manipulating Data With SQL Rating: 4 out of 5 stars4/5Mastering ChatGPT: 21 Prompts Templates for Effortless Writing Rating: 4 out of 5 stars4/5Elon Musk Rating: 4 out of 5 stars4/5How to Create Cpn Numbers the Right way: A Step by Step Guide to Creating cpn Numbers Legally Rating: 4 out of 5 stars4/5Technical Writing For Dummies Rating: 0 out of 5 stars0 ratings101 Awesome Builds: Minecraft® Secrets from the World's Greatest Crafters Rating: 4 out of 5 stars4/5CompTIA Security+ Get Certified Get Ahead: SY0-701 Study Guide Rating: 5 out of 5 stars5/5Deep Search: How to Explore the Internet More Effectively Rating: 5 out of 5 stars5/5The Innovators: How a Group of Hackers, Geniuses, and Geeks Created the Digital Revolution Rating: 4 out of 5 stars4/5The Self-Taught Computer Scientist: The Beginner's Guide to Data Structures & Algorithms Rating: 0 out of 5 stars0 ratingsThe Professional Voiceover Handbook: Voiceover training, #1 Rating: 5 out of 5 stars5/5Everybody Lies: Big Data, New Data, and What the Internet Can Tell Us About Who We Really Are Rating: 4 out of 5 stars4/5CompTIA IT Fundamentals (ITF+) Study Guide: Exam FC0-U61 Rating: 0 out of 5 stars0 ratingsLearning the Chess Openings Rating: 5 out of 5 stars5/5Slenderman: Online Obsession, Mental Illness, and the Violent Crime of Two Midwestern Girls Rating: 4 out of 5 stars4/5Creating Online Courses with ChatGPT | A Step-by-Step Guide with Prompt Templates Rating: 4 out of 5 stars4/5Tor and the Dark Art of Anonymity Rating: 5 out of 5 stars5/5Standard Deviations: Flawed Assumptions, Tortured Data, and Other Ways to Lie with Statistics Rating: 4 out of 5 stars4/5Procreate for Beginners: Introduction to Procreate for Drawing and Illustrating on the iPad Rating: 5 out of 5 stars5/5Becoming a Data Head: How to Think, Speak, and Understand Data Science, Statistics, and Machine Learning Rating: 5 out of 5 stars5/5Alan Turing: The Enigma: The Book That Inspired the Film The Imitation Game - Updated Edition Rating: 4 out of 5 stars4/5Excel 101: A Beginner's & Intermediate's Guide for Mastering the Quintessence of Microsoft Excel (2010-2019 & 365) in no time! Rating: 0 out of 5 stars0 ratingsUncanny Valley: A Memoir Rating: 4 out of 5 stars4/5
Reviews for Rust for C++ Programmers
0 ratings0 reviews
Book preview
Rust for C++ Programmers - Mustafif Khan
CHAPTER 1
Introduction to Rust
Introduction
Rust is a fairly new language that was developed from the ground up to be memory safe and has zero abstractions. Compared to other systems programming languages like C or C++, Rust offers close performance while avoiding memory issues like leaks, double free, or segmentation faults.
How you may ask? We will delve into detail later in this chapter, but Rust manages memory using binding’s lifetimes and ownership. Within the compiler, we have something called the borrow checker. Its role is to check ownership for each binding, and when out of scope, it is dropped. Ownership, in this case, means that each binding or variable owns its value, and when another binding takes that value, the previous is dropped.
If this seems overwhelming, do not worry; we will fight the borrow checker together, and we will learn how to love it. The compiler teaches us how to be smarter programmers, and thus make safer decisions when it comes to memory management.
This book assumes you have a fair knowledge of C++. We will compare code between Rust and C++ at certain points in the book and hope that it helps flatten the learning curve.
Structure
In this chapter, we will discuss the following topics:
Installing Rust
Getting started with Cargo
Bindings and Mutability
Ownership
Control Flow
Logic and conditional operators
If/Else statements
Match statements
Loops
While loops
For loops
Loop
Functions
Structs and Enums
Objectives
By the end of this chapter, the reader will be able to understand how to write simple programs in Rust using C++ as a translation layer, as well as get a general understanding of concepts like ownership and what it means to borrow a value. We will look into Rust’s package manager Cargo, which will help us create projects throughout the book, and see how the package manager can be used for benchmarking and unit testing.
Installing Rust
To install Rust, we will use the rustup tool, which allows us to change toolchains, update to stable, beta or nightly, and so on.
For Unix systems like Linux or macOS, you can enter the following command on your terminal:
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
On Windows systems, you will need to visit https://rustup.rs and install the setup file. However, make sure that you have C++ Build tools installed from Visual Studio.
Getting started with Cargo
Cargo is Rust’s package manager, and it allows developers to build projects and publish them. It is similar to pip for Python or npm for Node.js. Packages in Rust are referred to as crates and can be publicly found at https://crates.io. Let us create a project for our next section so we can see what Cargo offers us.
Bin versus Lib
Cargo has two types of projects that can be built: a binary or a library project. A binary project is a project that creates an executable and has a main.rs file, while a library is a collection of code that can be used in different projects and has a lib.rs file.
For our purposes, we will create a binary project. Run the following command on your terminal:
$ cargo new bindings_and_mut --bin
# for lib: cargo new
You will find a new directory named bindings_and_mut. Let us enter the directory and we will notice the following structure:
bindings_and_mut/
Cargo.lock
Cargo.toml
src/
main.rs
Let us look at what each of these files does in a Cargo project, so we are not confused when we create projects throughout the rest of the book:
Cargo.lock: Keeps a cache of the dependencies of a project.
Cargo.toml: Contains all the metadata of the project, as well as dependencies.
src/: The directory that contains all the source code for the project. If the project is a binary, the folder will contain a main.rs file while a library will contain a lib.rs.
Let us edit our Cargo.toml file so we can add the rand crate, and we can create a guessing game for the next section.
[package]
name = bindings_and_mut
version = 0.1.0
edition = 2021
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rand = 0.8.5
# add rand here
# generally it’s crate = version
# version is in form of major.minor.patch
Bindings and mutability
To begin this section, let us first look at a simple C++ program that shows variables of different data types. We will then follow with the same program but written in Rust:
// C++
int main(){
auto varOne = 76; //inferring integer
char* varTwo = var Two
; // C String
int varThree = 69; // integer
double varFour = 42.0; // floating point
return 0;
}
How can we create this program in Rust? First of all, how do we declare bindings?
We will use the let keyword. It is used to declare a binding. Binding’s data types in Rust are implicitly inferred but can be explicitly declared using the separator operator (a semicolon or ‘:’). So, let us recreate the program:
// Rust
fn main(){
let varOne = 76; // inferring integer
let varTwo: &str = var Two
; //borrowed string
let varThree: i32 = 69; // 32-bit integer
let varFour: f64 = 42.0; // 64-bit floating point
}
You may be a bit confused with the data types but that is fine. Table 1.1 shows each data type:
Table 1.1: Rust data types
Note: When a value is inferred and it is an integer, its type will be an i32. If the value is a floating point, the value is an f64 and string literals will be a borrowed string &str.
Strings seem confusing at first because there are two string types: borrowed (&str) and owned (String). We will go through this in detail when we discuss ownership, but the basic reason is that strings are heap-allocated values. Moreover, since their size cannot be determined during compile time, a borrowed string is essentially a reference to that heap value.
Guessing game
In our Cargo section, we conveniently created a project bindings_and_mut. Let us open it and start our simple guessing game. The game will first ask the user to enter a number. We will compare the number to the randomly generated answer and reveal the output if it is too high/low or correct. Let us begin with asking and getting user input:
fn main() {
// Ask the user for input
println!(Please enter a number from 1-20:
);
// Create a new empty string instance
let mut input = String::new();
// read the input from standard input
std::io::stdin()
.read_line(&mut input)
.expect(Couldn't get input.
);
Wait for a moment here. There’s a lot of mystery in these five lines of code. Let us resolve each mystery one line at a time.
println! is what we call a declarative macro, and it is defined by a "!". It acts as a way to add formatting and arguments into println!. We will see these in a more detailed view when we discussed in Chapter 9, Metaprogramming.
In Rust, bindings are immutable by default. To make a binding mutable, we place mut beside let followed by the identifier. After that, we create a new string instance to allow us to mutably borrow it for the standard input. This can be seen in the code here:
let mut input = String::new();
Why are Rust bindings immutable? Immutable Rust bindings improve security and safety in applications since values just can’t be changed. Apart from that, most of the time bindings don’t need to be mutable.
To read the input, we use the standard library’s standard input (std::io::stdin()). With this, we use the method read_line(&mut input) that requires a mutable reference to our binding. When we place the expect (Couldn’t get input.
) at the end of our binding, input prints a message if the program panics:
std::io::stdin()
.read_line(&mut input)
.expect(Couldn't get input.
);
Now, let us continue with the next snippet:
// trim and parse the input to an int
let guess: i32 = input.trim().parse().expect(Couldn't convert to integer
);
// make random number with range 1 to 20
let mut rng = rand::thread_rng(); // random generator
let answer: i32 = rng.gen_range(1..20); // creates number from range 1 - 20
// use match to compare the value
match guess.cmp(&answer){
// if guess is greater it's too high
Ordering::Greater => println!(Too high!
),
// if guess is lower it's too low
Ordering::Less => println!(Too low!
),
// if guess is equal it's correct
Ordering::Equal => println!(Guess Correct!!!
)
}
Before we continue with the explanation, we have some imports to do at the top of the file:
use rand::Rng; // random number generator
use std::cmp::Ordering; // compares each value to compare
fn main() {
…
}
First, let us look at how we convert our input to guess:
let guess: i32 = input.trim().parse().expect(Couldn't convert to integer
);
First, we trim the string. This means removing any extra white space, and after that, we use parse(). When using this method, a type must be specified, either explicitly as we did here or using the turbo-fish method .parse::
After having our guess, we need to start getting our answer ready. To do that, we use a random generator using the rand crate. To create a random generator, we use thread_rng() and then pass in a range 1..20 to generate the answer:
let mut rng = rand::thread_rng();
let answer: i32 = rng.gen_range(1..20);
To compare the guess to the answer, we rely on the cmp() method that returns the Ordering enum. We utilize the match statement to look at each case (Less, Greater, Equal), similar to how one would towards a switch statement. We can see this being done as follows:
match guess.cmp(&answer){
// if guess is greater it's too high
Ordering::Greater => println!(Too high!
),
// if guess is lower it's too low
Ordering::Less => println!(Too low!
),
// if guess is equal it's correct
Ordering::Equal => println!(Guess Correct!!!
)
}
This project gives us a good outlook on what we expect to see in this chapter. You may now run cargo run that will execute the binary or cargo build that will build the binary at target/debug/bindings_and_mut.
Ownership
Let us now look at the various aspects of ownership in Rust.
How Rust manages memory
Rust manages memory differently compared to other languages such as Python or Java that rely on garbage collectors. While Garbage Collectors nicely take care of memory management for the user, it creates an overhead during runtime to mark and blacken objects (a technique used for the garbage collector for Mufi-Lang). Rust being a zero abstractions language, having a garbage collector is unacceptable, so how do we manage memory? Do we do it manually like C/C++? Well yes and no; it is called the borrow checker!
Rust uses the notion of ownership to manage memory and utilizes the borrow checker during compile time. This may increase the time during compilation, but if it does compile, it will most likely work during runtime (take it with a grain of salt).
Before we dive any deeper, let us consider how Rust’s memory management compares to C++. Well, Rust and C++ both use a form of Resource Acquisition is Initialization (RAII). In Rust’s case, since bindings are stored on the stack, to allocate any value from the heap we need to use a smart pointer. The smart pointers all have different purposes; whether it is heap allocation (Box), reference counter (RC), Cell (Internal Mutability), and so on.
Owning and borrowing values
Owning or borrowing a value has the same goal of having a binding value, and the difference only lies in whether you want to take over it or not.
Borrowing a value is to have a reference to a value, whereas taking ownership of a value is becoming a new owner. To borrow, we use the & operator, which can also be considered a safe pointer (where * is a raw pointer). So, let us jump into some examples of borrowing and owning values:
let string = Hi, who owns me?
.to_string();
// makes &str to String
let borrowed_string = &string; // type: &str
println!({}
, &string);
println!({}
, borrowed_string);
If we run this code, we can see the output as follows:
Hi, who owns me?
Hi, who owns me?
Let us recreate the example but instead of having borrowed_string, we will have owned_string. Will we be able to print both bindings? (spoiler alert: no!):
let string = Hi, who owns me?
.to_string();
// makes &str to String
let owned_string = string; // type: String
println!({}
, &string);
println!({}
, owned_string);
If we run this code, we can see the following error, as follows:
error[E0382]: borrow of moved value: `string`
--> test.rs:5:16
|
2 | let string=Hi, who owns me?
.to_string();
| ------ move occurs because `string` has type `String`, which does not implement the `Copy` trait
3 | // makes &str to String
4 | let owned_string=string; // type: String
| ------ value moved here
5 | println!({}
, &string);
| ^^^^^^^ value borrowed here after move
error: aborting due to previous error
For more information about this error, try `rustc --explain E0382`.
This is because once a new owner takes ownership of a value, the old binding is dropped. But what if we want to keep it? Well, that ties into our next topic, the Copy and Clone traits.
Copy and clone
The copy and clone traits are used to copy a binding’s value, while copy only does this by taking a reference to the value and can be done implicitly or by borrowing a value. Clone is explicit by using the clone() method and creates a duplicate owner to a binding.
While we look at using the clone() method, let us shoot two birds with one stone and discuss scope. For this example, we will use a Reference Counter (std::rc::Rc) that counts the number of references made for a binding, and once it reaches 0, the binding is dropped.
In this example, we will create clones of binding and show the count of owners when we create the new clone and when we are about to leave it. Since Rc is a strong reference compared to Weak (which is a weak reference, hence the name), we will use std::rc::Rc::strong_count() to count the number of strong references of the owner:
use std::rc::Rc; //import reference counter
fn main(){
let owner = Rc::new(8); //create a new reference counter
println!(Owners: {}
, Rc::strong_count(&owner));
{ // create a closure using {}
println!(New closure
);
let owner2 = owner.clone(); // clone of owner
// to access value within, you can use *owner2
println!(Owners: {}
, Rc::strong_count(&owner));
{ // new closure
println!(New closure
);
let owner3 =owner.clone(); // clone of owner
println!(Owners: {}
, Rc::strong_count(&owner));
println!(Leaving closure, owner3 dropped
);
} // owner 3 drops out of scope
println!(Owners: {}
, Rc::strong_count(&owner));
println!(Leaving closure, owner2 dropped
);
} // owner 2 drops out of scope
println!(Owners: {}
, Rc::strong_count(&owner));
} // owner drops out of scope
As you can notice, once we leave the closure, the binding is dropped and the strong count will decrease. If we run this code, we will see the following output:
Owners: 1
New closure
Owners: 2
New closure
Owners: 3
Leaving closure, owner3 dropped
Owners: 2
Leaving closure, owner2 dropped
Owners: 1
Note: Values don’t always implement Copy; some only implement Clone or Copy & Clone (since Copy depends on Clone). This will make more sense when we discuss structures and enums in OOP, where we will discuss deriving traits from them.
Borrowing rules
To help avoid fighting against the borrow checker, it is best to know the rules of borrowing. To start off, we will talk about the different syntaxes for immutable and mutable borrowing:
&T: Immutable Borrow
Cannot change values of borrowed values
Simply a reference to a binding’s value
&mut T: Mutable Borrow
Can change the values of a borrowed value
Requires the binding to be mutable
Rules
A reference may not live longer than its owner.
For obvious reasons, if the owner is dropped, the reference of the value points to deallocated memory; in other words, to invalid memory (segmentation fault).
If there’s a mutable borrow to a value, no other references are allowed in the same scope.
Best way to think of this is as a sort of Mutex lock to a value.
If no mutable borrows exist, any number of immutable borrows can exist in the same scope.
Since the value isn’t mutably changing, this immutable borrow cannot affect the owner.
By following these rules, you should be able to get along with the borrow checker better, and someday, you will be best friends forever as segmentation faults give you death stares.
Pointer types
We will give an overview of the different pointer types, and these will be explained in greater detail in the latter chapters of the book.
&: Reference or Safe Pointer
Used to borrow a value
*: Dereference or Raw Pointer
Used to dereference a pointer
Mainly used in unsafe code
Box
Used to allocate values on the heap
Owns the value inside
Rc
Used for reference counting
Creates strong references of a value
Can be downgraded to provide a weak reference to a value
Value drops once reference count reaches 0
Arc
Atomic reference counting
Thread safe unlike Rc
Cell
Gives internal mutability to types that implement Copy
Allows for multiple mutable references
RefCell
Gives internal mutability without requiring the Copy trait
Uses runtime locking for safety
Control flow
This section on control flow will be very familiar since Rust follows a C syntax. So, as a reminder, logic operators are used to evaluate a Boolean expression. Conditional operators are used to compare values, and these two operators come in the following forms.
Logic operators
&& (and)
true && true = true
true && false = false
false && false = true
|| (or)
true || false = true
true || true = true
false || false = false
! (not)
!false = true
!true = false
Conditional operators
== (Equal Equal)
Compares values if they are equal to each other
<= (Less than or Equal to)
Compares values if they are less than or equal to each other
< (Less than)
Compares values if they are less than each other
>= (Greater than or Equal to)
Compares values if they are greater than or equal to each other
> (Greater than)
Compares values if they are greater than each other
If/Else Statements
If statements evaluate a block of code if the condition is true. Unlike in C++ or C, if statements in Rust do not require any parentheses:
if 5 > 3{
println!(TRUE
);
}
// Output: TRUE
// 5 is greater than 3
Unlike if statements, else statements evaluate if the condition is false. Else can be thought of as a default option, but its use cases depend on the context of the control flow:
if 5 < 3{
println!(TRUE
);
} else {
println!(FALSE
);
}
// Output: FALSE
// 5 is not less than 3
An else if statement is used to add another clause after the if statement, it will only execute as long as the other statements are false and it is true:
if 5 < 3{
println!(TRUE
);
}
else if 7 > 5{
println!(ALSO TRUE
);
}
else {
println!(FALSE
);
}
// Output: ALSO TRUE
// 7 is greater than 5
Match statements
Match statements are the replacements to switch statements, and they say goodbye to adding a break; to the end of each case. They follow a simple pattern-matching syntax without the unnecessary case keyword as shown in the following example:
match Foo{
Bar => {…},
…
_ => // default option
}
As much as Foo Bar shows us the general idea of the syntax, it is better if we dive into a deeper example:
// ask for favourite colour
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.expect(Couldn’t get input
);
// trim and make it lowercase
input = input.trim().to_lower();
// match input
match &input{
purple
=> println!(Good choice
),
blue
=> println!(Close to purple
),
red
=> println!(Close to purple as well
),
_ => println!(Why have you not picked purple?
);
}
// this program might be biased
Depending on what you input, a case will be printed.
Loops
Loops are used to execute code repeatedly until a condition is met. In Rust, we have three main loop statements, while, for, and loop. While the first two are quite familiar, the last isn’t commonly used, so we will not look into it in greater detail, compared to the others.
While loops
While loops execute until a condition becomes true. They work the same like in C++ and can be used in a simple example like this:
let mut sum = 0;
let mut i = 0;
while i < 50{
sum += i;
i += 1;
}
In Rust, we have no increment operator (++) like in C++, so to increment, we use +=. This while loop will keep running until i becomes 50, and we can also use keywords like continue or break to add special conditions inside the loop.
For loops
For loops are closer to how they work in Python than C++. We can either iterate through an iterable object (for i in &list) or through a range (for i in 0..20). Let us see this in practice and introduce vectors while we are doing this:
// Create a vector list
let vector = vec![1, 3, 5, 7]; // type: Vec
// Iterate and print each element
for i in &vector{
// i is type &i32
println!({}
, i);
}
// Output:
1
3
5
7
So, what’s going on? First, let us talk about how Rust implements vectors or dynamic arrays. You can either declare a vector with elements like the above (vec![…]) that uses the vec! macro which simply pushes each of the elements into the vector, or we create a new empty vector using Vec::new().
To push or pop values in a vector, you must make the binding mutable. To explicitly declare a type for the vector, we use the syntax Vec
When we iterate through the vector, we borrow it so i becomes an &i32 type and goes through each value in the vector.
Let us switch things up and use for with a range and push elements into a vector and create another for loop to print each element. This is completely inefficient, but we are doing this for learning purposes:
// Create the vector
let mut vector: Vec
// Push values from 0..10
for i in 0..10{
vector.push(i);
}
// Create loop to print each element
for i in 0..vector.len(){
println!(Element => {} : Value => {}
, &i, &vector[i]);
}
//Output:
Element => 0 : Value => 0
Element => 1 : Value => 1
Element => 2 : Value => 2
Element => 3 : Value => 3
Element => 4 : Value => 4
Element => 5 : Value => 5
Element => 6 : Value => 6
Element => 7 : Value => 7
Element => 8 : Value => 8
Element => 9 : Value => 9
Similar to Python, we declare a variable and assign it to a range using in. Instead of using range(0, T), we use 0..T which defines the range from 0 to T - 1. To get the length of the vector, we use the method .len() that is implied in the vector instead of using something like sizeof(vector).
Loop Statements
Sometimes, we do want an infinite loop and instead of using while true or for(;;) in Rust, we use loop. This isn’t popular to use (since infinite loops aren’t recommended), but it is still useful to know just in case it matches your needs.
So how do we declare it? Since loop statements are infinite loops, there are no conditions and are just followed by a block expression. It is up to the code in the loop to break or return at some point:
// initialize our count
let mut count = 0;
// declare loop
loop{
// when count is greater than 10, we get out
if count>10{
println!(GET OUT!!!!
);
break;
}
println!(Count at {}
, &count);
count += 1;
}
Functions
Let us now discuss functions in Rust.
Declaring functions
Finally, we get to have some fun with new materials; functions in Rust are different enough from C++ that we will look at translating a C++ function into Rust.
To declare functions in Rust, we use the fn keyword followed by an identifier to name the function, parameters in parentheses, an arrow then a return type if present. To put it simply, follow this general syntax:
fn foo(bar: &type, baz: type, laz: &mut type) -> return_type{}
Even if that doesn’t look the most pleasant, let us look at a C++ Fibonacci function and then recreate it in Rust:
// C++
int fib(int n){
if(n <= 1) return 1;
return fib(n-1) + fib(n-2);
}
// Rust
fn fib(n: u32) –> u32{
if (n <= 1) {
1
}
fib(n-1) + fib(n-2)
}
You’re probably wondering where the return statement is. In Rust, the last statement in a function implicitly returns; you can use return explicitly but it is more common to use implicit returns. In this book, we will use implicit returns a lot, but I will make sure to add a comment that we are returning the value in case you get lost in the code. In modern C++, this can also be replicated using the auto keyword which will infer a variable’s data type:
auto fib(auto n) -> int{
if (n <= 1) return 1;
return fib(n-1) + fib(n-2);
}
Note: Implicit returns must be the same type as the function; if not, there will be a compile error.
In parameters, you must specify if you are taking ownership, immutable borrow, or mutable borrow. For example, if we were to do the following:
fn take_ownership(s: String){
println!(Now we have ownership of {}
, s)
}
fn main(){
let s = I am bob the string
.to_string();
take_ownership(s);
// attempt to print s
println!({}
, s);
}
If we run the program, we get the following error, as follows:
error[E0382]: borrow of moved value: `s`
--> test.rs:9:20
|
6 | let s = I am bob the string
.to_string();
| - move occurs because `s` has type `String`, which does not implement the `Copy` trait
7 | take_ownership(s);
| - value moved here
8 | // attempt to print s
9 | println!({}
, s);
| ^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0382`.
Modules and publicity
In Rust, you can organize code in modules, and when importing code from other files, they are considered modules. To create a module, use the keyword mod, followed by an identifier and then a block statement. Inside the block, all functions, structs, and so on. are part of the module.
By default, functions in Rust are private and publicity must be explicitly stated using the keyword pub. This improves security since it allows fine control of what the user can and cannot access.
Super versus self
How do you access functions within the module? To do that, you can use self to access functions. To do this, we use the syntax, self::
pub mod test{
// adds hello to string
fn add_hello(str: &str)->String{
let mut owned = str.to_owned();
owned.push_str( hello
);
owned
}
// adds world to string
fn add_world(str: &str) ->String{
let mut owned = str.to_owned();
owned.push_str( world
);
owned
}
pub fn hello_world(s: &str){
// adds hello
Let with_hello = self::add_hello(s);
// adds world
let hw = self::add_world(&with_hello);
// print the masterpiece
println!({}
, hw)
}
}
fn main(){
let s = I am Bob!!!
;
// access test using test::
test::hello_world(s);
}
We create our public module test. Here, we have two private functions called add_hello and add_world that simply push hello
or world
into the string, respectively. In our public function hello_world, we can be seen accessing the functions using self::add_hello or self::add_world. To access the module inside our main function, we use test::hello_world.
If we run the code, we get the following:
I am Bob!!! hello world
Modules can be nested inside each other. That is where super comes in! Let us change our previous example a bit, so we have hello_world inside a nested module best_quotes:
pub mod test{
// adds hello to string
fn add_hello(str: &str)->String{
let mut owned = str.to_owned();
owned.push_str( hello
);
owned
}
// adds world to string
fn add_world(str: &str) ->String{
let mut owned = str.to_owned();
owned.push_str( world
);
owned
}
pub mod best_quotes{
pub fn hello_world(s: &str){
// adds hello
let with_hello = super::add_hello(s);
// adds world
let hw = super::add_world(&with_hello);
// print the masterpiece
println!({}
, hw)
}
}
}
fn main(){
let s = I am Bob!!!
;
// access test using test::
test::best_quotes::hello_world(s);
}
As you can see, not much has changed except that instead of using self, we use super and when using hello_world, we instead need to put test::best_quotes::hello_world.
Testing and benchmarking
Testing and benchmarking functions are very useful when performing changes or optimizations. For this section, we will have two implementations of a Fibonacci function, and we will have tests on it, then benchmark, and compare the two. For this topic, we will create a library for our testing and benchmarking. Let us open up our terminal and get started:
$ cargo new —lib test_and_bench
$ cd test_and_bench
# Create file for our fib functions
$ touch src/fib.rs
# Create benches directory for benchmarking
$ mkdir benches
# Create our fib bench file
$ touch benches/fib_bench.rs
Let us edit our Cargo.toml and add the following changes under [dependencies]:
# For benchmarks
[dev-dependencies]
criterion = 0.3
[[bench]]
name = fib_bench
harness = false
Our Fibonacci functions will differ quite a bit: one will follow a concurrent model by using thread spawning, while the other will just be a regular if/else statement. These functions will be written in src/fib.rs while we will write the tests in src/lib.rs.
Here is how our first Fibonacci function, fib_one looks like:
// import threads from standard lib
use std::thread::*;
pub fn