5.5 Operators

Operators in Rust allow you to perform operations on variables and values. Rust provides a wide range of operators, including unary, binary, and assignment operators, similar to C and C++. However, there are some key differences, such as the absence of certain operators like ++ and --. In this section, we will cover Rust’s operators in detail, explain operator precedence, and compare them to those in C/C++. We will also explore how to define custom operators in Rust.

5.5.1 Unary Operators

Unary operators operate on a single operand. Rust provides the following unary operators:

  • Negation (-): Negates the value of a number.
    • Example: -x
  • Logical negation (!): Inverts the value of a boolean.
    • Example: !true evaluates to false
  • Dereference (*): Dereferences a reference to access the underlying value.
    • Example: *pointer
  • Reference (&): Creates a reference to a value.
    • Example: &x creates a reference to x.

Example program using unary operators:

fn main() { // editable example
    let x = 5;
    let neg_x = -x;
    let is_false = !true;
    let reference = &x;
    let deref_x = *reference;

    println!("Negation of {} is {}", x, neg_x);
    println!("The opposite of true is {}", is_false);
    println!("Reference to x is: {:?}", reference);
    println!("Dereferenced value is: {}", deref_x);
}

5.5.2 Binary Operators

Binary operators in Rust work on two operands. These include arithmetic, logical, comparison, and bitwise operators.

Arithmetic Operators

  • Addition (+): Adds two values.
  • Subtraction (-): Subtracts the second value from the first.
  • Multiplication (*): Multiplies two values.
  • Division (/): Divides the first value by the second (integer division for integers).
  • Modulus (%): Finds the remainder after division.

Example:

fn main() {
    let a = 10;
    let b = 3;
    let sum = a + b;
    let difference = a - b;
    let product = a * b;
    let quotient = a / b;
    let remainder = a % b;

    println!("{} + {} = {}", a, b, sum);
    println!("{} - {} = {}", a, b, difference);
    println!("{} * {} = {}", a, b, product);
    println!("{} / {} = {}", a, b, quotient);
    println!("{} % {} = {}", a, b, remainder);
}

Note that Rust's binary arithmetic operators generally require both operands to have the same type, meaning expressions like 1u8 + 2i32 or 1.0 + 2 are invalid.

Comparison Operators

  • Equal to (==): Checks if two values are equal.
  • Not equal to (!=): Checks if two values are not equal.
  • Greater than (>): Checks if the first value is greater than the second.
  • Less than (<): Checks if the first value is less than the second.
  • Greater than or equal to (>=): Checks if the first value is greater than or equal to the second.
  • Less than or equal to (<=): Checks if the first value is less than or equal to the second.

These operators work on integers, floating-point numbers, and other comparable types.

Example:

fn main() {
    let x = 5;
    let y = 10;

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

Logical Operators

  • Logical AND (&&): Returns true if both operands are true.
  • Logical OR (||): Returns true if at least one operand is true.

Example:

fn main() {
    let a = true;
    let b = false;

    println!("a && b: {}", a && b);
    println!("a || b: {}", a || b);
}

Bitwise Operators

  • Bitwise AND (&): Performs a bitwise AND operation.
  • Bitwise OR (|): Performs a bitwise OR operation.
  • Bitwise XOR (^): Performs a bitwise XOR operation.
  • Left shift (<<): Shifts the bits of the left operand to the left by the number of positions specified by the right operand.
  • Right shift (>>): Shifts the bits of the left operand to the right by the number of positions specified by the right operand.

For shift operations, there is a key distinction between signed and unsigned integer types. For unsigned types, right shifts fill the leftmost bits with zeros. For signed types, right shifts use sign extension, meaning that the leftmost bit (the sign bit) is preserved, which maintains the negative or positive sign of the number.

Example:

fn main() {
    let x: u8 = 2;  // 0000_0010 in binary
    let y: u8 = 3;  // 0000_0011 in binary

    println!("x & y: {}", x & y); // 0000_0010
    println!("x | y: {}", x | y); // 0000_0011
    println!("x ^ y: {}", x ^ y); // 0000_0001
    println!("x << 1: {}", x << 1); // 0000_0100
    println!("x >> 1: {}", x >> 1); // 0000_0001

    let z: i8 = -2; // 1111_1110 in binary (signed)
    println!("z >> 1 (signed): {}", z >> 1); // Sign bit is preserved: 1111_1111
}

5.5.3 Assignment Operators

The assignment operator in Rust is the equal sign (=), which is used to assign values to variables. Rust also supports compound assignment operators, which combine arithmetic or bitwise operations with assignment:

  • Add and assign (+=): x += 1;
  • Subtract and assign (-=): x -= 1;
  • Multiply and assign (*=): x *= 1;
  • Divide and assign (/=): x /= 1;
  • Modulus and assign (%=): x %= 1;
  • Bitwise AND and assign (&=): x &= y;
  • Bitwise OR and assign (|=): x |= y;
  • Bitwise XOR and assign (^=): x ^= y;
  • Left shift and assign (<<=): x <<= y;
  • Right shift and assign (>>=): x >>= y;

Example:

#![allow(unused)]
fn main() {
let mut x = 5;
x += 2;
println!("x after addition: {}", x);
}

5.5.4 Ternary Operator

Rust does not have a traditional ternary operator like C's ? :. Instead, Rust uses if expressions that can return values, making the ternary operator unnecessary.

Example of an if expression in Rust:

#![allow(unused)]
fn main() {
let condition = true;
let result = if condition { 5 } else { 10 };
println!("The result is: {}", result);
}

5.5.5 Custom Operators and Operator Overloading

Unlike C++, Rust does not allow defining new custom operators (e.g., using special Unicode characters). However, Rust does support operator overloading through traits. You can implement Rust's built-in traits, like Add, to define custom behavior for existing operators.

Example: Overloading the + operator for a custom type.

use std::ops::Add;

struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = Point { x: 3, y: 4 };
    let p3 = p1 + p2;  // Uses the overloaded + operator
    println!("p3: x = {}, y = {}", p3.x, p3.y);
}

In this example, the + operator is overloaded for the Point struct by implementing the Add trait. This allows two Point instances to be added using the + operator.

5.5.6 Operator Precedence

Operator precedence in Rust determines the order in which operations are evaluated. Rust’s precedence rules are similar to those in C and C++, with multiplication and division taking precedence over addition and subtraction, and parentheses () being used to control the order of evaluation.

Here is a simplified operator precedence table (from highest to lowest precedence):

  1. Method call and field access: .
  2. Function call and array indexing: () and []
  3. Unary operators: -, !, *, &
  4. Multiplicative: *, /, %
  5. Additive: +, -
  6. Bitwise shifts: <<, >>
  7. Bitwise AND: &
  8. Bitwise XOR: ^
  9. Bitwise OR: |
  10. Comparison and equality: ==, !=, <, <=, >, >=
  11. Logical AND: &&
  12. Logical OR: ||
  13. Range operators: .., ..=
  14. Assignment and compound assignment: =, +=, -=, etc.

Example:

#![allow(unused)]
fn main() {
let result = 2 + 3 * 4;
println!("Result without parentheses: {}", result);  // Outputs 14
let result_with_parentheses = (2 + 3) * 4;
println!("Result with parentheses: {}", result_with_parentheses);  // Outputs 20
}

5.5.7 Comparison with C and C++

Rust’s operators are quite similar to those in C and C++. However, Rust lacks the ++ and -- operators, which increment or decrement variables in C/C++. This design decision in Rust prevents unintended side effects and encourages clearer code, requiring you to use += 1 or -= 1 explicitly for incrementing or decrementing values.