| Contents |

5 Common programming concepts

This chapter introduces fundamental programming concepts shared by most languages, illustrating how they function in Rust and drawing comparisons with C where relevant. We will cover keywords, identifiers, expressions and statements, core data types (including scalar types, tuples, and arrays), variables (focusing on mutability, constants, and statics), operators, numeric literals, arithmetic overflow behavior, performance aspects of numeric types, and comments.

While many concepts will feel familiar to C programmers, Rust’s handling of types, mutability, and expressions often introduces stricter rules for enhanced safety and clarity. We defer detailed discussion of control flow (like if and loops) and functions until after covering memory management, as these constructs frequently interact with Rust’s ownership model. Similarly, Rust’s struct and powerful enum types, along with standard library collections like vectors and strings, will be detailed in dedicated later chapters.


5.1 Keywords

Keywords are predefined, reserved words with special meanings in the Rust language. They form the building blocks of syntax and cannot be used as identifiers (like variable or function names) unless escaped using the raw identifier syntax (r#keyword). Many Rust keywords overlap with C/C++, but Rust adds several unique ones to support features like ownership, borrowing, pattern matching, and concurrency.

5.1.1 Raw Identifiers

Occasionally, you might need to use an identifier that conflicts with a Rust keyword. This often happens when interfacing with C libraries or using older Rust code (crates) written before a word became a keyword in a newer Rust edition.

To resolve this, Rust provides raw identifiers: prefix the identifier with r#. This tells the compiler to treat the following word strictly as an identifier, ignoring its keyword status.

For example, if a C library exports a function named try (a reserved keyword in Rust), you would call it as r#try() in your Rust code. Similarly, if Rust introduces a new keyword like gen (as in the 2024 edition) that was used as a function or variable name in an older crate you depend on, you can use r#gen to refer to the item from the old crate.

fn main() {
    // 'match' is a keyword, used for pattern matching.
    // To use it as a variable name, we need `r#`.
    let r#match = "Keyword used as identifier";
    println!("{}", r#match);

    // 'type' is also a keyword.
    struct Example {
        r#type: i32, // Use raw identifier for field name
    }
    let instance = Example { r#type: 1 };
    println!("Field value: {}", instance.r#type);

    // 'example' is NOT a keyword. Using r# is allowed but unnecessary.
    // Both 'example' and 'r#example' refer to the same identifier.
    let example = 5;
    let r#example = 10; // This shadows the previous 'example'.
    println!("Example: {}", example); // Prints 10

    // Note: Inside format strings like println!, use the identifier *without* r#.
    // println!("{}", r#match); // This would be a compile error.
}

While you can use r# with non-keywords, it’s generally only needed for actual keyword conflicts or, rarely, for future-proofing if you suspect an identifier might become a keyword later.

5.1.2 Keyword Categories

Rust classifies keywords into three groups:

  1. Strict Keywords: Actively used by the language and always reserved.
  2. Reserved Keywords: Reserved for potential future language features; currently unused but cannot be identifiers.
  3. Weak Keywords: Have special meaning only in specific syntactic contexts; can be used as identifiers elsewhere.

5.1.3 Strict Keywords

These keywords have defined meanings and cannot be used as identifiers without r#.

Keyword Description C/C++ Equivalent (Approximate)
as Type casting, renaming imports (use path::item as new_name;) (type)value, static_cast
async Marks a function or block as asynchronous C++20 co_await context
await Pauses execution until an async operation completes C++20 co_await
break Exits a loop or block prematurely break
const Declares compile-time constants const
continue Skips the current loop iteration continue
crate Refers to the current crate root None
dyn Used with trait objects for dynamic dispatch Virtual functions (indirectly)
else The alternative branch for an if or if let expression else
enum Declares an enumeration (sum type) enum
extern Links to external code (FFI), specifies ABI extern "C"
false Boolean literal false false (C++), 0 (C)
fn Declares a function Function definition syntax
for Loops over an iterator for, range-based for (C++)
gen Reserved (Rust 2024+, experimental generators) C++20 coroutines
if Conditional expression if
impl Implements methods or traits for a type Class methods (C++), None (C)
in Part of for loop syntax (for item in iterator) Range-based for (C++)
let Introduces a variable declaration Declaration syntax (no direct keyword)
loop Creates an unconditional, infinite loop while(1), for(;;)
match Pattern matching expression switch (less powerful)
mod Declares a module Namespaces (C++), None (C)
move Forces capture-by-value in closures Lambda captures (C++)
mut Marks a variable or reference as mutable No direct C equivalent (const is inverse)
pub Makes an item public (visible outside its module) public: (C++ classes)
ref Binds by reference within a pattern & in patterns (C++)
return Returns a value from a function early return
Self Refers to the implementing type within impl or trait blocks Current class type (C++)
self Refers to the instance in methods (&self, &mut self, self) this pointer (C++)
static Defines static items (global lifetime) or static lifetimes static
struct Declares a structure (product type) struct
super Refers to the parent module .. in paths (conceptual)
trait Declares a trait (shared interface/behavior) Abstract base class (C++), Interface (conceptual)
true Boolean literal true true (C++), non-zero (C)
type Defines a type alias or associated type in traits typedef, using (C++)
unsafe Marks a block or function with relaxed safety checks C code is implicitly unsafe
use Imports items into the current scope #include, using namespace
where Specifies constraints on generic types requires (C++20 Concepts)
while Loops based on a condition while

5.1.4 Reserved Keywords (For Future Use)

These are currently unused but reserved for potential future syntax. Avoid using them as identifiers.

Reserved Keyword Potential Use Area C/C++ Equivalent (Possible)
abstract Abstract types/methods virtual ... = 0; (C++)
become Tail calls? None
box Custom heap pointers std::unique_ptr (concept)
do do-while loop? do
final Prevent overriding final (C++)
macro Alternative macro system? #define (concept)
override Explicit method override override (C++)
priv Private visibility? private: (C++)
try Error handling syntax try (C++)
typeof Type introspection? typeof (GNU C), decltype (C++)
unsized Dynamically sized types None
virtual Virtual dispatch virtual (C++)
yield Generators/coroutines co_yield (C++20)

5.1.5 Weak Keywords

These words have special meaning only in specific contexts. Outside these contexts, they can be used as identifiers without r#.

5.1.6 Comparison with C/C++

While C programmers will recognize keywords like if, else, while, for, struct, enum, const, and static, Rust introduces many new ones. Keywords like let, mut, match, mod, crate, use, impl, trait, async, await, and unsafe reflect Rust’s different approaches to variable declaration, mutability control, pattern matching, modularity, interfaces, asynchronous programming, and safety boundaries. The ownership system itself doesn’t have dedicated keywords but relies on how let, mut, fn signatures, and lifetimes interact.


5.2 Identifiers and Allowed Characters

Identifiers are names given to entities like variables, functions, types, modules, etc. In Rust:

  1. Allowed Characters: Identifiers must start with a Unicode character belonging to the XID_Start category or an underscore (_). Subsequent characters can be from XID_Start, XID_Continue, or _.
  2. Restrictions:
  3. Encoding: Identifiers must be valid UTF-8.
  4. Length: No explicit length limit, but overly long identifiers harm readability.

Naming Conventions (Style, Not Enforced by Compiler):

These conventions enhance readability and are strongly recommended.


5.3 Expressions and Statements

Rust makes a clearer distinction between expressions and statements than C/C++.

5.3.1 Expressions

An expression evaluates to a value. Most code constructs in Rust are expressions, including:

// These are all expressions:
5
x + 1
is_valid(data)
if condition { value1 } else { value2 }
{ // This whole block is an expression
    let intermediate = compute();
    intermediate * 10 // The block evaluates to this value
}

Critically, an expression by itself is not usually valid Rust code. It needs to be part of a statement (like an assignment or a function call) or used where a value is expected (like the right side of = or a function argument).

5.3.2 Statements

A statement performs an action but does not evaluate to a useful value.

Common statement types:

  1. Declaration Statements: Introduce items like variables, functions, structs, etc.
  2. Expression Statements: An expression followed by a semicolon. This is used when you care only about the side effect of the expression (like calling a function that modifies state or performs I/O) and want to discard its return value. The semicolon effectively discards the value of the preceding expression.

Key Difference from C/C++: Assignment (=) is a statement in Rust, not an expression. It does not evaluate to the assigned value. This prevents code like x = y = 5; (which works in C) and avoids potential bugs related to assignment within conditional expressions (if (x = 0)).

let y = 10; // Declaration statement
x = y + 5; // Assignment statement (the expr. y + 5 is evaluated, then assigned to x)
do_something(); // Expression statement (calls function, discards result)

5.3.3 Block Expressions

In Rust, a code block enclosed in curly braces { ... } is an expression that itself evaluates to a value.

fn main() {
    let y = {
        let x = 3;
        x + 1 // No semicolon: the block evaluates to x + 1 (which is 4)
    };
    println!("y = {}", y); // Prints: y = 4

    let z = {
        let x = 3;
        x + 1; // Semicolon: the value is discarded, block evaluates to ()
    };
    println!("z = {:?}", z); // Prints: z = ()

    let w = { }; // Empty block evaluates to ()
    println!("w = {:?}", w); // Prints: w = ()
}

This feature is powerful, allowing if, match, and even simple blocks to be used directly in assignments or function arguments. Be mindful of the final semicolon; omitting or adding it changes the block’s resulting value and type.

5.3.4 Line Structure

Rust is free-form regarding whitespace and line breaks. Statements are terminated by semicolons, not newlines.

// Valid, spans multiple lines
let sum = 10 + 20 +
          30 + 40;

// Valid, multiple statements on one line (discouraged for readability)
let a = 1; let b = 2; println!("Sum: {}", a + b);

5.4 Data Types

Rust is statically typed, meaning the type of every variable must be known at compile time. It is also strongly typed, generally preventing implicit type conversions between unrelated types (e.g., integer to float requires an explicit as cast). This catches many errors early.

Rust’s data types fall into several categories. Here we cover scalar and basic compound types.

5.4.1 Scalar Types

Scalar types represent single values.

Scalar Type Summary Table:

Rust Type Size (bits) Range / Representation C Equivalent (<stdint.h>) Notes
i8 8 -128 to 127 int8_t Signed 8-bit
u8 8 0 to 255 uint8_t Unsigned 8-bit (often used for byte data)
i16 16 -32,768 to 32,767 int16_t Signed 16-bit
u16 16 0 to 65,535 uint16_t Unsigned 16-bit
i32 32 -2,147,483,648 to 2,147,483,647 int32_t Default integer type
u32 32 0 to 4,294,967,295 uint32_t Unsigned 32-bit
i64 64 Approx. -9.2e18 to 9.2e18 int64_t Signed 64-bit
u64 64 0 to approx. 1.8e19 uint64_t Unsigned 64-bit
i128 128 Approx. -1.7e38 to 1.7e38 __int128_t (compiler ext.) Signed 128-bit
u128 128 0 to approx. 3.4e38 __uint128_t (compiler ext.) Unsigned 128-bit
isize Arch-dependent (32/64) Arch-dependent intptr_t Signed pointer-sized integer
usize Arch-dependent (32/64) Arch-dependent uintptr_t, size_t Unsigned pointer-sized, used for indexing
f32 32 (IEEE 754) Single-precision float float
f64 64 (IEEE 754) Double-precision float double Default float type
bool 8 (usually) true or false _Bool / bool (<stdbool.h>) Boolean value
char 32 Unicode Scalar Value (U+0000..U+10FFFF, excl. surrogates) wchar_t (varies), char32_t (C++) Represents a Unicode character (4 bytes)

5.4.2 Compound Types

Compound types group multiple values into one type. Rust has two primitive compound types: tuples and arrays.

Tuple

A tuple is an ordered, fixed-size collection of values where each element can have a different type. Tuples are useful for grouping related data without the formality of defining a struct.

fn main() {
    // A tuple with an i32, f64, and u8
    let tup: (i32, f64, u8) = (500, 6.4, 1);

    // Access elements using period and index (0-based)
    let five_hundred = tup.0;
    let six_point_four = tup.1;
    let one = tup.2;
    println!("Tuple elements: {}, {}, {}", five_hundred, six_point_four, one);

    // Tuple elements must be accessed with literal indices (0, 1, 2, ...).
    // You cannot use a variable index like tup[i] or tup.variable_index.
    // const IDX: usize = 1;
    // let element = tup.IDX; // Compile Error

    // Tuples can be mutable if declared with 'mut'
    let mut mutable_tup = (10, "hello");
    mutable_tup.0 = 20; // OK
    println!("Mutable tuple: {:?}", mutable_tup);

    // Destructuring: Extract values into separate variables
    let (x, y, z) = tup; // Assigns tup.0 to x, tup.1 to y, tup.2 to z
    println!("Destructured: x={}, y={}, z={}", x, y, z);
}

Accessing tuple fields by index (e.g., tup.0) is extremely efficient. The compiler calculates the exact memory offset at compile time, resulting in a direct memory access with no runtime overhead, similar in performance to accessing struct fields in C.

Tuples are good for returning multiple values from a function or when you need a simple, anonymous grouping of data. For more complex data with meaningful field names, use a struct.

Array

An array is a fixed-size collection where every element must have the same type. Arrays are stored contiguously in memory on the stack (unless part of a heap-allocated structure).

fn main() {
    // Array of 5 integers
    let numbers: [i32; 5] = [1, 2, 3, 4, 5];

    // Type and length can often be inferred
    let inferred_numbers = [10, 20, 30]; // Inferred as [i32; 3]

    // Initialize with a default value
    let zeros = [0u8; 10]; // Array of 10 bytes, all zero

    // Access elements (0-based index, must be usize)
    let first = numbers[0];
    let third = numbers[2];
    println!("First: {}, Third: {}", first, third);

    // Index must be usize
    let idx: usize = 1;
    println!("Element at index {}: {}", idx, numbers[idx]);

    // let invalid_idx: i32 = 1;
    // println!("{}", numbers[invalid_idx]); // Compile Error: index must be usize

    // Bounds checking (this would panic if uncommented)
    // println!("Out of bounds: {}", numbers[10]);

    // Arrays can be mutable
    let mut mutable_array = [1, 1, 1];
    mutable_array[1] = 2;
    println!("Mutable array: {:?}", mutable_array);

    // Get length
    println!("Length of numbers: {}", numbers.len()); // 5
}

Array element access (array[index]) using a runtime variable index is typically very fast. It involves a simple calculation to find the element’s memory address (base + index * size). Crucially, safe Rust precedes this access with a runtime bounds check (index < array.len()) to ensure memory safety, preventing buffer overflows common in C. While this check adds a minimal runtime overhead compared to C’s unchecked access, it provides a vital safety guarantee.

However, if the index is a compile-time constant (e.g., array[2] or an index defined via const), the compiler performs a static bounds check. An out-of-bounds constant index will result in a compilation error, preventing any runtime check. In such cases, the access compiles down to a direct memory operation with a known offset, making it as efficient as accessing a tuple or struct field.

Use arrays when you know the exact number of elements at compile time and need a simple, fixed-size sequence. For dynamically sized collections, use Vec<T> (vector) from the standard library (covered later).

Multidimensional Arrays

You can create multidimensional arrays in Rust by nesting array declarations. For example, a 2x3 matrix (2 rows, 3 columns) can be represented as an array of 2 elements, where each element is an array of 3 integers:

fn main() {
    let matrix: [[i32; 3]; 2] = [ // Type: array of 2 elements, each [i32; 3]
        [1, 2, 3], // Row 0: An array of 3 i32s
        [4, 5, 6], // Row 1: An array of 3 i32s
    ];

    // Accessing element at row 1, column 2 (0-based index)
    let element = matrix[1][2]; // Accesses the value 6
    println!("Element at [1][2]: {}", element);

    // You can also modify elements if the matrix is mutable
    let mut mutable_matrix = matrix;
    // Copies the original matrix (since [i32; 3] and [[i32; 3]; 2] are Copy)
    mutable_matrix[0][1] = 20; // Change element at row 0, column 1 to 20
    println!("Modified matrix[0][1]: {}", mutable_matrix[0][1]); // Prints 20
    println!("Original matrix[0][1]: {}", matrix[0][1]);
    // Prints 2 (original is unchanged)
}

This demonstrates creating an array of arrays. Accessing elements uses chained indexing (matrix[row][column]), and standard bounds checking applies at each level.

5.4.3 References

As introduced in Chapter 2, Rust provides references—safe, managed pointers that allow indirect access to data stored elsewhere in memory. Much like pointers in C, references contain the memory address of a value, enabling one level of indirection.

References in Rust come in two forms: immutable and mutable. They make it possible to temporarily access data without taking ownership or creating a copy, which is particularly efficient when passing values to functions.

To create a reference, Rust uses the & symbol for immutable access and &mut for mutable access. The dereferencing operator * can be used to access the value behind a reference, though Rust often applies dereferencing automatically when needed. In principle, it’s possible to create references to references (e.g., &&value), introducing multiple levels of indirection, but this is seldom required in practice.

Rust also supports raw pointers, which can be used within unsafe blocks for low-level operations that are not checked by the compiler.

Chapter 6 will explore references more thoroughly as part of the discussion on Ownership, Borrowing, and Memory Management.

The following example demonstrates a function that takes a mutable reference to a fixed-size array and squares each element in place:

fn square_elements(arr: &mut [i32; 5]) {
    for i in 0..arr.len() {
        arr[i] *= arr[i];
    }
}

fn main() {
    let mut numbers = [1, 2, 3, 4, 5];
    square_elements(&mut numbers);
    println!("{:?}", numbers); // [1, 4, 9, 16, 25]
}

The function modifies the original array by working directly on its elements through a mutable reference. This avoids the overhead of copying data into and out of the function.

5.4.4 Stack vs. Heap Allocation (Brief Overview)

By default, local variables holding scalar types, tuples, and arrays are allocated on the stack. Stack allocation is very fast because it involves just adjusting a pointer. The size of stack-allocated data must be known at compile time.

Data whose size might change or is not known until runtime (like the contents of a Vec<T> or String) is typically allocated on the heap. Heap allocation is more flexible but involves more overhead (finding free space, bookkeeping).

We will explore stack, heap, ownership, and borrowing—concepts central to Rust’s memory management—in detail in later chapters. For now, understand that primitive types like those discussed here are usually stack-allocated when used as local variables.

5.4.5 A Note on Sub-Range Types

Coming from languages like Ada, Pascal, or Nim, you might be familiar with defining integer types restricted to a specific sub-range, such as type Month = 1..12;. Rust does not have direct, built-in syntax for creating such custom integer types where the range constraint is automatically enforced by the type system on all assignments and operations. This generally aligns with Rust’s philosophy of providing powerful, composable building blocks (like structs and enums) rather than adding numerous specialized types to the language core.

When you need to ensure a number consistently stays within a specific range in Rust, idiomatic approaches include:

Interestingly, while Rust lacks general integer sub-range types, the language and standard library do heavily utilize the concept of value restriction – particularly non-nullness or non-zero-ness – to enhance safety and enable crucial optimizations:

So, while you won’t find a direct equivalent to type Day = 1..31;, Rust provides patterns to achieve similar guarantees and leverages specific range restrictions (like non-zero) where they offer substantial benefits.


5.5 Variables and Mutability

Variables associate names with data stored in memory.

5.5.1 Declaring Variables

Use the let keyword to declare a variable and initialize it.

let message = "Hello"; // Declare 'message', initialize it with "Hello"
let count = 10;       // Declare 'count', initialize it with 10

A Note on Terminology: “Binding”

You will frequently encounter the term “binding” in Rust literature (e.g., “variable binding,” “let binds a value to a name”). This term emphasizes that let creates an association between a name and a value or memory location.

While accurate, especially when discussing immutability, shadowing, or references, the term “binding” might feel slightly abstract for simple cases like let x: i32 = 5; if you’re used to C’s model where the variable x is the memory location holding 5. In such simple cases, thinking of let as declaring a variable and initializing it with a value is perfectly valid and perhaps more direct.

This chapter will often use simpler terms like “declare,” “initialize,” “assign,” or “holds a value” for basic variable operations, while reserving “binding” for contexts like immutability or shadowing where it adds clarity. Be aware that other Rust resources heavily use “binding” in all contexts.

5.5.2 Immutability by Default

By default, variables declared with let are immutable. Once initialized, their value cannot be changed.

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    // x = 6; // Compile Error: cannot assign twice to immutable variable `x`
}

This design choice encourages safer code by preventing accidental modifications and making program state easier to reason about, especially important for concurrency. We refer to let x = 5; as creating an immutable binding.

5.5.3 Mutable Variables

To allow a variable’s value to be changed after initialization, declare it using let mut.

fn main() {
    let mut y = 10;
    println!("The initial value of y is: {}", y);
    y = 11; // OK, because y was declared as mutable
    println!("The new value of y is: {}", y);
}

Use mut deliberately when you need to change a variable’s value. Prefer immutability when possible.

5.5.4 Type Annotations and Inference

Rust’s compiler features powerful type inference. It can usually determine the variable’s type automatically based on the initial value and how the variable is used later.

let inferred_integer = 42;       // Inferred as i32 (default integer type)
let inferred_float = 2.718;     // Inferred as f64 (default float type)

However, you can (and sometimes must) provide an explicit type annotation using a colon (:) after the variable name.

let explicit_float: f64 = 3.14;  // Explicitly typed as f64
let count: u32 = 0;              // Explicitly typed as u32

// Annotation needed when type isn't clear from initializer or context
let guess: u32 = "42".parse().expect("Not a number!");

// Annotation needed if declared without immediate initialization
let later_initialized: i32;
later_initialized = 100; // OK now

Annotations are required when the compiler cannot uniquely determine the variable’s type from its initialization and usage context (a common example is with functions like parse() which can return different types based on the annotation).

5.5.5 Uninitialized Variables

Rust guarantees, through compile-time checks, that you cannot use a variable before it has been definitely initialized on all possible code paths.

fn main() {
    let x: i32; // Declared but not initialized

    let condition = true;
    if condition {
        x = 1; // Initialized on this path
    } else {
        // If we comment out the line below, the compiler will complain
        // because 'x' might not be initialized before the println!.
        x = 2; // Initialized on this path too
    }

    // OK: The compiler knows 'x' is guaranteed to be initialized by this point.
    println!("The value of x is: {}", x);

    // let y: i32;
    // println!("{}", y); // Compile Error: use of possibly uninitialized variable `y`
}

This check eliminates a common source of bugs found in C/C++ related to reading uninitialized memory. Note that compound types like tuples, arrays, and structs must generally be fully initialized at once; partial initialization is usually not permitted for safe Rust code.

5.5.6 Constants

Constants represent values that are fixed for the entire program execution and are known at compile time. They are declared using the const keyword.

const SECONDS_IN_MINUTE: u32 = 60;
const MAX_USERS: usize = 1000;

fn main() {
    let total_seconds = 5 * SECONDS_IN_MINUTE;
    println!("Five minutes is {} seconds.", total_seconds);

    let user_ids = [0u32; MAX_USERS]; // Use const for array size
    println!("Max users allowed: {}", MAX_USERS);
    println!("User ID array size: {}", user_ids.len());
}

Use const for values that are truly fixed, program-wide parameters or mathematical constants.

5.5.7 Static Variables

In Rust, global variables—that is, variables declared outside of any function—are called static variables. Like constants, static variables (static) represent values that exist for the entire duration of the program ('static lifetime). However, unlike const, they occupy a fixed, single memory address. Any access to a static variable involves reading from or writing to this specific memory location, similar to how global variables function in C.

Static variables:

Important Note on Rust Edition 2024: References to static mut

With Rust Edition 2024, the compiler now defaults to disallowing shared or mutable references to static mut variables, even within unsafe blocks. This change stems from the static_mut_refs lint being set to deny by default. The rationale is that taking such references leads to immediate undefined behavior, even if the reference is never actually used. This occurs because maintaining Rust’s strict mutability XOR aliasing rule (either one mutable reference or many shared references, but not both) becomes exceptionally difficult with global mutable state, particularly in multithreaded or reentrant contexts.

Consider the following examples, which will now result in a compilation error in Rust Edition 2024:

static mut X: i32 = 23;
static mut Y: i32 = 24;
unsafe {
    let y = &X;             // ERROR: shared reference to mutable static
    let ref x = X;          // ERROR: shared reference to mutable static
    let (x, y) = (&X, &Y);  // ERROR: shared reference to mutable static
}

static mut NUMS: &[u8; 3] = &[0, 1, 2];
unsafe {
    println!("{NUMS:?}");   // ERROR: shared reference to mutable static
    let n = NUMS.len();     // ERROR: shared reference to mutable static
}

This error also applies to implicit references, such as those created when printing static mut variables or calling methods on them, as shown by the println! and len() examples above.

Workarounds for static mut References

For situations where a reference to a static mut variable is genuinely necessary—for instance, in specific embedded programming scenarios where the overhead of Mutex or atomics might be unacceptable—there are two primary workarounds:

  1. Using Raw Pointers: You can obtain a raw pointer to the static mut variable. Raw pointers in Rust do not come with the same aliasing guarantees as references and require manual dereferencing within an unsafe block.

    static mut GLOBAL_COUNTER: u32 = 0;
    
    fn main() {
        unsafe {
            // Obtain a raw mutable pointer
            let ptr: *mut u32 = &raw mut GLOBAL_COUNTER;
            *ptr += 1; // Dereference the raw pointer to modify
    
            // Obtain a raw constant pointer to read
            let const_ptr: *const u32 = &raw const GLOBAL_COUNTER;
            println!("COUNTER (via raw pointer): {}", *const_ptr);
        }
    }

    As illustrated, a raw pointer can be created from a static mut item using &raw mut or &raw const and then dereferenced to access the value. This explicitly signals to the compiler and other developers that you are operating outside of Rust’s usual reference safety guarantees.

  2. Allowing the Lint: You can explicitly disable the static_mut_refs lint for specific code sections or for the entire crate. This approach should be used with extreme caution, as it bypasses a critical safety check.

    To allow the lint for the entire crate, add the following attribute at the top of your main.rs or lib.rs file:

    #![allow(static_mut_refs)]
    
    static mut REQUEST_COUNTER: u32 = 0;
    
    fn main() {
        unsafe {
            // This will now compile due to the #![allow] attribute
            let ref_to_counter = &REQUEST_COUNTER;
            println!("Requests processed (unsafe with allowed lint): {}",
            ref_to_counter);
        }
    }

    While suppressing the error, this method does not eliminate the underlying undefined behavior. Therefore, it is only advisable when you have a profound understanding of memory safety and aliasing in your specific use case.

Here’s an example illustrating the proper use of static and safer alternatives to static mut:

// Immutable static: lives for the program duration at a fixed address.
static APP_VERSION: &str = "1.0.2";

// Mutable static: requires unsafe to access.
// As of Rust 2024, taking references to this is disallowed by default.
static mut REQUEST_COUNTER: u32 = 0;

fn main() {
    println!("Running version: {}", APP_VERSION);

    // Accessing/modifying static mut requires an unsafe block.
    // In Rust 2024, taking a direct reference like `&REQUEST_COUNTER`
    // would now result in a compilation error by default.
    unsafe {
        // Direct modification is still allowed within unsafe
        REQUEST_COUNTER += 1;

        // Not allowed in Rust 2024:
        // println!("Requests processed (unsafe direct access): {}", REQUEST_COUNTER);

        // Example of accessing via raw pointer (to bypass Rust 2024 reference lint)
        let raw_ptr_counter: *const u32 = &raw const REQUEST_COUNTER;
        println!("Requests processed (unsafe via raw pointer): {}", *raw_ptr_counter);
    }

    increment_safe_counter(); // Prefer safe alternatives
}

// A safer way to handle global mutable state using atomics
use std::sync::atomic::{AtomicU32, Ordering};
static SAFE_COUNTER: AtomicU32 = AtomicU32::new(0);

fn increment_safe_counter() {
    // Atomically increment the counter
    SAFE_COUNTER.fetch_add(1, Ordering::Relaxed);
    println!("Requests processed (safe using atomics): {}",
    SAFE_COUNTER.load(Ordering::Relaxed));
}

Alternatives to mutable global variables are discussed later in Chapter 19, “Smart Pointers,” and the use of raw pointers is discussed in detail in Chapter 25, where we cover unsafe language extensions.

const vs. static

5.5.8 Shadowing

Rust allows you to declare a new variable with the same name as a previously declared variable within the same or an inner scope. This is called shadowing. The new variable declaration creates a new binding, making the previous variable inaccessible by that name from that point forward (or temporarily, within an inner scope).

fn main() {
    let x = 5;
    println!("x = {}", x); // Prints 5

    // Shadow x by creating a new variable also named x
    let x = x + 1; // This 'x' is a new variable, initialized using the old 'x'
    println!("Shadowed x = {}", x); // Prints 6

    {
        // Shadow x again in an inner scope
        let x = x * 2; // This is yet another 'x', local to this block
        println!("Inner shadowed x = {}", x); // Prints 12
    } // Inner scope ends, its 'x' binding disappears

    // We are back to the 'x' from the outer scope (the one holding 6)
    println!("Outer x after scope = {}", x); // Prints 6

    // Shadowing is often used to transform a value while reusing its name,
    // potentially even changing the type.
    let spaces = "   ";       // 'spaces' holds a &str (string slice)
    let spaces = spaces.len(); // The name 'spaces' is re-bound to a usize value
    println!("Number of spaces: {}", spaces); // Prints 3
}

Shadowing differs significantly from marking a variable mut. Mutating (let mut y = 5; y = 6;) changes the value within the same variable’s memory location, without changing its type. Shadowing (let x = 5; let x = x + 1;) creates a completely new variable (potentially with a different type) that happens to reuse the same name, making the old variable inaccessible by that name afterwards.

5.5.9 Scope and Lifetimes

A variable is valid (or “in scope”) from the point it’s declared until the end of the block {} in which it was declared. When a variable goes out of scope, Rust automatically calls any necessary cleanup code for that variable (this is part of the ownership and RAII system, detailed later).

fn main() { // Outer scope starts
    let outer_var = 1;
    { // Inner scope starts
        let inner_var = 2;
        println!("Inside inner scope: outer={}, inner={}", outer_var, inner_var);
    } // Inner scope ends, 'inner_var' goes out of scope and is cleaned up

    // println!("Outside inner scope: inner={}", inner_var);
    // Compile Error: `inner_var` not found in this scope
    println!("Back in outer scope: outer={}", outer_var);
} // Outer scope ends, 'outer_var' goes out of scope and is cleaned up

5.5.10 Declaring Multiple Variables (Destructuring)

While C allows int a, b;, Rust typically uses one let statement per variable. However, Rust supports destructuring assignment using patterns, which is often used with tuples or structs to initialize multiple variables at once.

fn main() {
    let (x, y) = (5, 10); // Destructure the tuple (5, 10)
                         // This binds x to 5 and y to 10

    println!("x={}, y={}", x, y);
}

We will see more advanced uses of patterns and destructuring later.


5.6 Operators

Rust supports most standard operators familiar from C/C++.

Key Differences/Notes for C Programmers:

  1. No Increment/Decrement Operators: Rust does not have ++ or --. Use x += 1 or x -= 1 instead. This avoids ambiguities present in C regarding pre/post increment/decrement return values and side effects within expressions.

  2. Strict Type Matching: Binary operators (like +, *, &, ==) generally require operands of the exact same type. Implicit numeric promotions like in C (e.g., int + float) do not happen. You must explicitly cast using as.

    let a: i32 = 10;
    let b: u8 = 5;
    // let c = a + b; // Compile Error: mismatched types i32 and u8
    let c = a + (b as i32); // OK: b is explicitly cast to i32
    println!("c = {}", c);
  3. No Ternary Operator: Rust does not have C’s condition ? value_if_true : value_if_false. Use an if expression instead, which is more readable and less prone to precedence errors:

    let result = if condition { 5 } else { 10 };
    println!("Result = {}", result);
  4. Operator Overloading: You cannot create new custom operators, but you can overload existing operators (like +, -, *, ==) for your own custom types (structs, enums) by implementing corresponding traits from the std::ops module (e.g., Add, Sub, Mul, PartialEq). This allows operators to work intuitively with user-defined types like vectors or complex numbers.

In addition to the operators mentioned earlier, Rust uses & to create references or to specify that a type is a reference, and * to dereference a reference in order to access the original value it points to.

Operator Precedence: Largely follows C/C++ conventions (e.g., * and / before + and -, comparisons before logical operators). Use parentheses () to clarify or force a specific evaluation order when in doubt – clarity is usually preferred over relying on subtle precedence rules.


5.7 Numeric Literals

Numeric literals allow you to specify fixed numeric values directly in your source code.

fn main() {
    let decimal = 100_000;       // i32 by default
    let hex = 0xEADBEEF;        // i32 by default
    let octal = 0o77;            // i32 by default
    let binary = 0b1101_0101;   // i32 by default
    let byte = b'X';             // u8 (value 88)

    let float_def = 3.14;        // f64 by default
    let float_f32 = 2.718f32;    // f32 explicit suffix
    let float_exp = 6.022e23;    // f64

    println!("Dec: {}, Hex: {}, Oct: {}, Bin: {}, Byte: {}",
    decimal, hex, octal, binary, byte);
    println!("f64: {}, f32: {}, Exp: {}", float_def, float_f32, float_exp);

    // Type inference example:
    let values: [f32; 3] = [1.0, 2.0, 3.0]; //Lit. are known to be f32 from array type
    let sum = values[0] + 0.5;//0.5 here must be f32 due to context, suffix not needed
    println!("Sum (f32): {}", sum);

    let value_f64 = 1.0; // f64
    // let mixed_sum = values[0] + value_f64; // Error: mismatched types f32 and f64
}

If the compiler cannot unambiguously determine the required numeric type from the context (e.g., assigning to an untyped variable, or initial parsing), you must provide either a type suffix on the literal or a type annotation on the variable.


5.8 Overflow in Arithmetic Operations

Integer overflow occurs when an arithmetic operation results in a value outside the representable range for its type. C/C++ behavior for signed overflow is often undefined, leading to subtle bugs and security vulnerabilities. Rust provides well-defined, safer behavior.

// Example (behavior depends on build mode: debug vs release)
fn main() {
   let max_u8: u8 = 255;

   // This line's behavior changes:
   // - Debug: Panics with "attempt to add with overflow"
   // - Release: Wraps around, result becomes 0
   let result = max_u8 + 1;

   println!("Result: {}", result); // Only runs in release mode without panic
}

This difference means code relying on wrapping behavior might panic unexpectedly in debug builds, while code assuming panics won’t happen might produce incorrect results due to wrapping in release builds.

5.8.1 Explicit Overflow Handling

To ensure consistent and predictable behavior regardless of build mode, Rust provides methods on integer types for explicit overflow control:

Choose the method that best reflects the intended logic for calculations that might exceed the type’s bounds. Relying on the default build-mode-dependent behavior is often risky.

5.8.2 Floating-Point Overflow

Floating-point types (f32, f64) adhere to the IEEE 754 standard for arithmetic and do not panic or wrap on overflow. Instead, operations exceeding representable limits produce special values:

fn main() {
    let x = 1.0f64 / 0.0; // Positive Infinity
    let y = -1.0f64 / 0.0; // Negative Infinity
    let z = 0.0f64 / 0.0; // NaN

    println!("x = {}, y = {}, z = {}", x, y, z);

    // Use methods to check for these special values
    println!("x is infinite: {}", x.is_infinite()); // true
    println!("x is finite: {}", x.is_finite());   // false
    println!("y is infinite: {}", y.is_infinite()); // true
    println!("z is NaN: {}", z.is_nan());         // true

    // Crucial NaN comparison behavior: NaN is not equal to anything, including itself!
    println!("z == z: {}", z == z); // false! Use is_nan() instead.
}

Code involving floating-point arithmetic should be prepared to handle Infinity and especially NaN. Remember that direct equality checks (==) with NaN always return false; use the .is_nan() method instead.


5.9 Performance Considerations for Numeric Types

Different numeric types offer trade-offs between memory usage, value range, and computational performance.

General Advice: Begin with the defaults (i32, f64). Choose other types based on specific requirements: range needs (i64, u64, i128), memory constraints (i8, u16, f32), or indexing/memory size representation (usize). If performance is critical, profile your code rather than making assumptions about the speed of different types. Be mindful that explicit as casts between numeric types, while necessary for type safety, are not entirely free and represent computations that take some amount of time.


5.10 Comments in Rust

Comments are annotations within the source code ignored by the compiler but essential for human understanding. They should explain the why behind code, document assumptions, or clarify complex sections.

5.10.1 Regular Comments

Used for explanatory notes within function bodies or alongside specific lines of code.

  1. Single-line comments: Start with // and extend to the end of the line. Ideal for brief notes.

    // Calculate the average of the two values
    let average = (value1 + value2) / 2.0; // Use floating-point division
  2. Multi-line comments (Block comments): Start with /* and end with */. They can span multiple lines and are useful for longer explanations or temporarily disabling blocks of code. Rust supports nested block comments.

    /*
       This function processes user input.
       It first validates the format, then updates the internal state.
       TODO: Add better error handling for malformed input.
       /* Nested comment example: Temporarily disable logging
       println!("Processing input: {}", input);
       */
    */
    fn process_input(input: &str) {
        // ... function body ...
    }

5.10.2 Documentation Comments

Special comments processed by the rustdoc tool to automatically generate HTML documentation for your crate (library or application). They use Markdown syntax internally.

  1. Outer doc comments (/// or /** ... */): Document the item that immediately follows them (e.g., a function, struct, enum, trait, module). This is the most common form, used for documenting public APIs.

    /// Represents a geometric point in 2D space.
    pub struct Point {
        /// The x-coordinate value.
        pub x: f64,
        /// The y-coordinate value.
        pub y: f64,
    }
    
    /**
     * Calculates the distance between two points.
     *
     * Uses the Pythagorean theorem.
     *
     * # Arguments
     *
     * * `p1` - The first point.
     * * `p2` - The second point.
     *
     * # Examples
     *
     * ```
     * let point1 = Point { x: 0.0, y: 0.0 };
     * let point2 = Point { x: 3.0, y: 4.0 };
     * assert_eq!(calculate_distance(&point1, &point2), 5.0);
     * ```
     */
    pub fn calculate_distance(p1: &Point, p2: &Point) -> f64 {
        ((p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sqrt()
    }
  2. Inner doc comments (//! or /*! ... */): Document the item that contains them – typically the module or the crate itself. These are usually placed at the very beginning of the file (lib.rs or main.rs for the crate documentation, mod.rs or the module’s file for module documentation).

    // In lib.rs or main.rs
    //! # Geometry Utilities Crate
    //!
    //! This crate provides basic types and functions for working with
    //! 2D geometry, such as points and distance calculations.
    
    // In utils/mod.rs
    /*!
      Internal utility functions module.
      Not part of the public API.
    */

Guidelines:


5.11 Summary

This chapter covered the foundational building blocks common to many programming languages, as implemented in Rust, highlighting key differences from C:

These concepts provide a necessary base for writing Rust programs. While some aspects resemble C, Rust’s emphasis on explicitness (like type casting and overflow handling), static guarantees (like initialization checks), and default immutability contribute significantly to its safety and reliability. The next chapters will delve into Rust’s unique ownership and borrowing system, showing how it interacts with functions, control flow, and data structures to provide memory safety without a garbage collector.



| Contents |