Polyglot
Languages Rust operators
Rust § operators

Operators

Rust’s operator surface is similar to C’s at the level of arithmetic and comparison, with substantial differences: integer overflow is panic in debug, wrap in release (with wrapping_*/checked_*/saturating_* methods for explicit choice), boolean operators are &&/||/! (no word-spelt forms), the question-mark operator ? admits compact error propagation, and the range operators (.., ..=) admit slice and range syntax. Operator overloading is admitted through traits (Add, Sub, Mul, etc.); the type-system constraints prevent ambiguity.

Arithmetic operators

The principal arithmetic operators:

let a = 7;
let b = 2;

a + b           // 9
a - b           // 5
a * b           // 14
a / b           // 3 (integer division; truncates toward zero for both signs)
a % b           // 1 (remainder)
-a              // negation; only on signed types

let c = 7.5;
let d = 2.0;
c / d           // 3.75 (floating-point division)

10_i32.pow(3);              // 1000 (integer power)
2.0_f64.powi(10);           // 1024.0 (integer exponent)
2.0_f64.powf(0.5);          // 1.41... (floating exponent)

Several distinguishing features:

Integer overflow

In debug builds, integer arithmetic panics on overflow:

let n: u8 = 255;
let m = n + 1;              // debug: panic!; release: wraps to 0

In release builds, overflow wraps (modular arithmetic). The behaviour is defined — no undefined behaviour — but differs between modes.

For explicit overflow handling:

let x: u8 = 200;

x.wrapping_add(100);        // wraps: 44
x.checked_add(100);         // Option<u8>; None on overflow
x.saturating_add(100);      // saturates at u8::MAX: 255
x.overflowing_add(100);     // (u8, bool); (44, true) on overflow

The four families admit explicit choice when overflow matters; they are conventional in numeric code that handles untrusted input.

No implicit numeric conversion

Rust does not implicitly convert between numeric types:

let a: i32 = 5;
let b: f64 = 3.14;
let sum = a + b;            // ERROR: type mismatch

let sum = a as f64 + b;     // OK: explicit cast
let sum = f64::from(a) + b; // OK: From trait

The strictness is one of Rust’s distinctive features; mixing types requires explicit conversion.

Comparison

a == b                       // equality
a != b                       // inequality
a < b
a <= b
a > b
a >= b

Comparison requires the PartialEq (or Eq) trait for equality and PartialOrd (or Ord) for ordering. Most primitive types and standard-library types implement these.

Floating-point types are PartialEq and PartialOrd but not Eq and Ord because of NaN — comparisons involving NaN return false:

let nan = f64::NAN;
nan == nan                   // false
nan < 1.0                    // false
nan > 1.0                    // false

For sorting floats, conventional patterns use partial_cmp with explicit NaN handling or the total_cmp method (since 1.62) for IEEE 754 total ordering.

Logical operators

true && false                // false (short-circuiting)
true || false                // true (short-circuiting)
!true                        // false

The logical operators && and || short-circuit. They operate only on bool; there is no truthiness coercion.

Bitwise operators

For integer types:

0b1010 & 0b1100              // 0b1000 (AND)
0b1010 | 0b1100              // 0b1110 (OR)
0b1010 ^ 0b1100              // 0b0110 (XOR)
!0b1010_u8                   // 0b1111_0101 (bitwise NOT; one's complement)
0b1010_u8 << 2               // 0b101000
0b1010_u8 >> 1               // 0b0101

The ! operator is bitwise NOT on integers and logical NOT on booleans; the operator is overloaded by type.

Reference operators

Rust’s reference operators:

let x = 5;
let r = &x;                  // shared reference
let n = *r;                  // dereference

let mut y = 10;
let m = &mut y;              // mutable reference
*m = 20;                     // assign through the mutable reference

The full treatment is in Ownership and borrowing.

Range operators

The .. and ..= admit ranges:

0..10                        // 0, 1, ..., 9 (exclusive)
0..=10                       // 0, 1, ..., 10 (inclusive)
..10                         // 0, ..., 9 (start unspecified)
0..                          // 0, 1, ... (end unspecified)
..                           // all (rare)

// Iteration:
for i in 0..10 {
    println!("{}", i);
}

// Slicing:
let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..4];      // [2, 3, 4]
let slice = &arr[..3];       // [1, 2, 3]
let slice = &arr[2..];       // [3, 4, 5]

The .. operator is the conventional Rust range syntax. In pattern matching, ..= is for inclusive ranges (1..=10); the half-open .. is rare in patterns.

The ? operator

The ? operator admits compact error propagation:

fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut f = File::open(path)?;
    let mut contents = String::new();
    f.read_to_string(&mut contents)?;
    Ok(contents)
}

The ? is roughly:

let mut f = match File::open(path) {
    Ok(f) => f,
    Err(e) => return Err(e),
};

If the operand is Err(e), ? returns Err(e) from the enclosing function; if Ok(value), it unwraps to value. The mechanism admits substantial conciseness in error-handling chains; treated in Error handling.

The ? also works for Option<T>: if None, returns None.

The dot operator

The . operator is method call and field access:

let p = Point { x: 3.0, y: 4.0 };
p.x;                         // field access
p.distance(&q);              // method call

let v = vec![1, 2, 3];
v.len();                     // method
v.iter();                    // method
v.iter().map(|n| n * 2).collect::<Vec<_>>();

The dot operator admits method resolution across the type’s methods, its traits’ default methods, and its parent traits’ methods. Rust’s deref coercion admits methods on T to be called on &T, &mut T, and Box<T> automatically:

let s = String::from("hello");
s.len();                     // String's method
let r: &String = &s;
r.len();                     // works through deref

Operator overloading

Operators are implemented through traits in std::ops. The principal traits:

OperatorTrait
+Add
- (binary)Sub
*Mul
/Div
%Rem
- (unary)Neg
!Not
& (bitwise)BitAnd
| (bitwise)BitOr
^BitXor
<<Shl
>>Shr
+=, -=, etc.AddAssign, SubAssign, etc.
==, !=PartialEq
<, <=, >, >=PartialOrd
[index]Index, IndexMut
use std::ops::Add;

#[derive(Clone, Copy)]
struct Money(i64);

impl Add for Money {
    type Output = Money;

    fn add(self, other: Money) -> Money {
        Money(self.0 + other.0)
    }
}

let a = Money(100);
let b = Money(50);
let total = a + b;           // Money(150)

The trait-based mechanism admits user-defined types to participate in arithmetic; treated in Traits.

Assignment

Assignment uses =:

let mut x = 5;
x = 10;                      // reassignment (mut required)

let mut v = vec![1, 2, 3];
v[0] = 100;                  // through indexing

Assignment moves the value (or copies for Copy types). The full treatment is in Ownership and borrowing.

Compound assignments dispatch through the *Assign traits:

let mut n = 5;
n += 1;                       // AddAssign
n -= 1;                       // SubAssign
n *= 2;
n /= 2;
n %= 2;
n <<= 1;
n &= 0xff;

The conditional expression

Rust does not have a ?: ternary; if … else is itself the conditional expression:

let max = if a > b { a } else { b };

The two arms must have the same type; the if expression’s value is the value of the selected arm. Treated in Conditionals.

as and casting

The as operator admits explicit conversion between primitive types:

let n: i32 = 42;
let m: i64 = n as i64;
let f: f64 = n as f64;

let big: u32 = 1_000_000;
let small: u8 = big as u8;          // truncates; 64 (low 8 bits)

let neg: i32 = -1;
let unsigned: u32 = neg as u32;     // u32::MAX (two's complement)

The as is infallible; it always produces a result, possibly truncating or producing a “weird” value. For checked conversions, use TryFrom:

let n: i32 = -5;
let u: Result<u32, _> = u32::try_from(n);  // Err(...)

Operator precedence

The full precedence table from highest to lowest:

LevelOperatorsAssociativity
1Paths, method calls, field access, function callsleft
2Question mark ?left
3Unary -, !, * (deref), &, &mutright
4asleft
5*, /, %left
6+, - (binary)left
7<<, >>left
8& (bitwise)left
9^left
10| (bitwise)left
11==, !=, <, >, <=, >=non-associative
12&&left
13||left
14.., ..= (range)left
15=, +=, etc. (assignment)right
16return, break (with value)(n/a)

Three precedence facts worth memorising:

  1. & and | (bitwise) bind looser than comparison; if (x & MASK) == 0 requires the parentheses.
  2. as binds tightly; x as u32 + 1 is (x as u32) + 1, not x as (u32 + 1).
  3. ? binds very tightly; f()? is the conventional form, with ? applied to the call result.

When in doubt, parenthesise. Rust does not penalise redundant parentheses; the alternative is to invite the bug.

A note on what Rust does not have

The operators that some other languages provide and Rust’s status:

OperatorAvailable?
Pre/post increment (++x, x++)No. Use x += 1.
&&=, ||=No. Use explicit if.
?: ternaryNo. Use if … else (an expression).
Pointer arithmeticNo (without unsafe).
Operator overloading by classYes (through traits).
<<< / unsigned right shiftNo. Cast to unsigned first.
** for exponentiationNo. Use pow / powi / powf methods.

The combination — arithmetic operators with explicit overflow handling, the ? operator for error propagation, range operators for slicing and iteration, trait-based overloading — is the substance of Rust’s operator surface.