Arithmetic, Bitwise, Comparison, and Logical Operators

How Rust uses arithmetic, bitwise, comparison, and logical operators to build expressions for calculations, value inspection, and decision-making

An operator is a symbol that tells Rust to perform a specific operation on one or more values. The values being operated on are called operands. Rust groups operators into several families; this page covers the four most fundamental: arithmetic, bitwise, comparison, and logical.

What separates Rust’s operator design from that of many other languages is its strictness. Types must usually match exactly before an operation is allowed, numeric overflow does not silently wrap by default, and logical operators refuse to work on anything other than bool. These rules prevent entire categories of bugs. The following sections explain each family in detail, how the rules work in practice, and the mistakes that trip people up the most.

Arithmetic Operators

Rust provides five arithmetic operators for the built-in numeric types (i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64):

OperatorOperationExample
+Additionx + y
-Subtractionx - y
*Multiplicationx * y
/Divisionx / y
%Remainderx % y

Unlike languages that quietly widen numbers for you, Rust demands that both operands have the same type. If you try to add an i32 and a u32, compilation will fail with a type mismatch error. The intent is to force you to make the conversion explicit rather than letting the compiler guess.

Integer Division and the Remainder

Division between two integers discards the fractional part — it truncates toward zero.

let a: i32 = 7;
let b: i32 = 3;
println!("{}", a / b); // prints 2

This behavior is identical to C and C++. The remainder operator % returns the remainder of the division, and for signed integers the sign of the result matches the sign of the left operand. So -7 % 3 is -1, not 2. That is different from a mathematical modulo operation, and it catches developers coming from Python or mathematics backgrounds.

println!("{}", -7 % 3);  // -1
println!("{}", 7 % -3);  // 1

Remainder follows the dividend:

If you need a proper mathematical modulo — where the result is always non‑negative — you must implement it manually or use the rem_euclid method, which computes the Euclidean remainder. The % operator alone does not give that guarantee.

Division by zero on integers triggers a runtime panic because Rust protects against undefined behavior at that level. Floating‑point division by zero, however, returns infinity or NaN depending on the numerator, following the IEEE 754 standard.

Overflow Behavior

By default, arithmetic overflow is a bug. In debug builds, an overflowing addition, subtraction, multiplication, or shift panics immediately. In release builds, the program continues but wraps using two’s complement arithmetic — meaning 255u8 + 1 becomes 0.

let x: u8 = 255;
// In debug: panics at runtime
// In release: x is now 0
let y = x + 1;

Overflow is a debug panic by default:

Shipping release code that accidentally overflows does not crash, but it produces wrong results silently. Rust provides wrapping_add, saturating_add, checked_add, and overflowing_add methods on integer types for cases where you want explicit control. If your algorithm depends on wrapping arithmetic, use the method to document the intent rather than relying on release‑mode behavior.

The conscious separation between debug panics and release wrapping is a design choice that catches bugs early during testing while preserving performance in production. It also means you should never test arithmetic only in release mode; the lack of a panic can hide a logic error.

A full arithmetic example

fn main() {
    let a: i32 = 10;
    let b: i32 = 4;
    println!("{} + {} = {}", a, b, a + b);  // 14
    println!("{} - {} = {}", a, b, a - b);  // 6
    println!("{} * {} = {}", a, b, a * b);  // 40
    println!("{} / {} = {}", a, b, a / b);  // 2
    println!("{} % {} = {}", a, b, a % b);  // 2
    let x: f64 = 10.0;
    let y: f64 = 4.0;
    println!("{} / {} = {}", x, y, x / y);  // 2.5
}

The code illustrates integer truncation versus floating‑point precision. When you run it, the integer division drops the .5 while the f64 division keeps it. Beginners often forget that 10 / 4 with integers is 2, not 2.5; the moment floating‑point precision matters, at least one operand must be a float.

Bitwise Operators

Bitwise operators treat an integer as a sequence of individual bits. They are the building blocks for low‑level manipulation: permissions flags, hardware registers, compact data structures, and performance‑sensitive arithmetic.

OperatorOperationExample
&Bitwise ANDa & b
|Bitwise ORa | b
^Bitwise XORa ^ b
!Bitwise NOT!a
<<Left shifta << n
>>Right shifta >> n

All bitwise operators can be used on the integer types and on bool. On integers they work on the binary representation. On bool they behave as non‑short‑circuiting logical operators (sometimes called “eager” logical operators). This dual role confuses many newcomers.

Bitwise operations on integers

Each bit of the operands is combined independently. The following example uses u8 values and shows the binary results explicitly.

fn main() {
    let a: u8 = 0b0011_1100; // 60
    let b: u8 = 0b0000_1111; // 15
    println!("a & b  = {:08b}", a & b); // 00001100 (12)
    println!("a | b  = {:08b}", a | b); // 00111111 (63)
    println!("a ^ b  = {:08b}", a ^ b); // 00110011 (51)
    println!("!a     = {:08b}", !a);    // 11000011 (195 in u8)
}

The bitwise NOT (!) flips every bit. Because u8 has eight bits, !60u8 becomes 195. If you use a signed integer like i8, ! still inverts the bits, but the interpretation is a two’s complement number; that is why !1i8 is -2.

Shift operators

Left shift (<<) moves bits to the left and fills the right‑hand side with zeros. Right shift (>>) behaves differently for signed and unsigned integers: on unsigned integers it is a logical shift (zero‑fill), and on signed integers it is an arithmetic shift (sign‑fill) that preserves the sign bit.

let n: u8 = 0b0000_0100; // 4
println!("{}", n << 2);   // 16  (binary 0001_0000)
let s: i8 = -16;          // binary 1111_0000 in two's complement
println!("{}", s >> 2);   // -4  (binary 1111_1100)

Rust also enforces safety on shift amounts. If the shift amount is greater than or equal to the number of bits in the type, the program panics in debug mode; in release mode it reduces the shift modulo the bit width, effectively masking the lower bits. This again avoids silent data corruption while still allowing fast release builds.

The overlap with logical operators

Rust allows & and | on bool as non‑short‑circuiting logical AND and OR. Unlike && and ||, they always evaluate both operands.

fn side_effect() -> bool {
    println!("side effect");
    true
}
let result = false & side_effect(); // prints "side effect", result is false
let result2 = false && side_effect(); // does NOT print, result2 is false

& and | are not the same as && and ||:

Using & where you meant && can cause unwanted side effects to execute and makes the intent of your code ambiguous. Always use && and || for logical conditions that should short‑circuit. Reserve & and | on booleans for the rare cases where you genuinely need both sides evaluated — for example, when both expressions must run for their side effects before you can make a decision.

Bitwise XOR (^) on booleans is also supported, but bitwise NOT (!) on a bool is not — use the logical NOT (!) which works identically. This small inconsistency often goes unnoticed because the logical ! already does what most people expect.

Comparison Operators

Comparison operators produce a bool by testing a relationship between two values. Rust offers the standard six:

OperatorMeaningExample
==Equal toa == b
!=Not equal toa != b
<Less thana < b
>Greater thana > b
<=Less than or equal toa <= b
>=Greater than or equal toa >= b

Under the hood, == and != require the type to implement the PartialEq trait, while the ordering operators require PartialOrd. Most standard library types automatically derive these traits, so you rarely need to think about them for built‑in types. Custom types you define can also derive them with #[derive(PartialEq, PartialOrd)].

Operand type matching

Like arithmetic operators, comparisons demand that both operands have the same type. You cannot compare an i32 to a u32 without an explicit cast.

let x: i32 = 10;
let y: u32 = 10;
// println!("{}", x == y); // error: mismatched types
println!("{}", x == y as i32); // works, prints true

Casting one operand to match the other is an explicit signal of intent. It makes auditing for accidental width mismatches possible at a glance.

Floating‑point NaN gets special treatment

The IEEE 754 standard mandates that NaN (Not a Number) is never equal to anything, including itself. This has practical consequences for comparison operators. The expression f64::NAN == f64::NAN evaluates to false, and f64::NAN != f64::NAN evaluates to true. Ordering comparisons with NaN always return false.

NaN breaks equivalence checks:

If you rely on equality to detect duplicate floating‑point values, NaN will evade detection every time. For data structures that must handle NaN, use the is_nan() method or the total_cmp method (on nightly) which provides a total ordering that places NaN consistently.

Logical Operators

Rust’s logical operators work exclusively on bool values. They are the primary tools for combining boolean conditions in if, while, and match guards.

OperatorOperationExample
&&Logical ANDa && b
||Logical ORa || b
!Logical NOT!a

Both && and || are short‑circuiting: the right‑hand operand is evaluated only if the left‑hand operand does not already determine the result. For &&, if the left side is false, the whole expression is false and the right side is skipped. For ||, if the left side is true, the right side is skipped.

fn expensive_check() -> bool {
    println!("expensive check ran");
    true
}
let a = false;
if a && expensive_check() {
    println!("both true");
}
// Nothing prints except the "expensive check ran"? Actually it won't run.
// In this case, a is false, so expensive_check() never executes.

Short‑circuiting is not just an optimization; it is a correctness tool. You can safely write ptr.is_some() && ptr.unwrap() > 5 because the right side only runs when the pointer is known to contain a value.

Boolean‑only operands prevent C‑style bugs

In C and C++, integers are truthy, so if (x & 4) is a common pattern but also a source of subtle bugs when a programmer accidentally types & instead of &&. Rust eliminates that entire class by requiring bool operands for logical operators.

let x: i32 = 8;
let y: i32 = 4;
// if x && y { ... } // error: expected `bool`, found integer
if x > 0 && y > 0 {
    println!("both positive");
}

The compiler forces you to write an explicit comparison, which makes your intent visible and reviewable. The bitwise & operator on integers is still available, but it produces an integer, not a bool, so you cannot accidentally drop it into a conditional position.

Using bool-only logic eliminates a major source of bugs:

If your code compiles, you know that every logical operator is working with genuine booleans. The compiler will not let you sneak an integer through. This is one of the less flashy but most valuable safety nets in the language.

Comparing Bitwise and Logical Operators Directly

A single operator symbol can mean very different things depending on the types of its operands. The most confusing pair is & (bitwise AND) versus && (logical AND), and | (bitwise OR) versus || (logical OR). The following table summarises the differences in Rust:

OperatorOperand typesShort‑circuits?Result type
&Integers or boolNoSame as operands
&&bool onlyYesbool
|Integers or boolNoSame as operands
||bool onlyYesbool

When you see & on two booleans, remember that both sides run unconditionally. That is occasionally useful — for example, when you need to update two separate counters inside the operands — but in everyday conditional logic, it is almost always a bug.

Rust did not invent this split, but it enforces it:

Many systems languages have both bitwise and logical operators. Rust’s contribution is making the logical variants bool‑only and refusing to compile code that blurs the line. This means you can search a codebase for && and know with certainty that it deals with boolean logic, never bit‑twiddling.

Summary

Rust’s operator families share a consistent philosophy: operations are explicit about types, overflow behavior is controlled, and logical operators guard against silent type coercion. Arithmetic operators let you compute, but they force you to handle overflow and type mismatches consciously. Bitwise operators give you low‑level bit manipulation with clear semantics for integers and a deliberate — though sometimes surprising — dual role on booleans. Comparison operators work uniformly across types that opt into the comparison traits, with special rules around NaN that mirror the floating‑point standard. Logical operators keep boolean logic clean by demanding actual bool values.

If you are reaching for any of these operators now, the single habit that will save you the most debugging time is this: when in doubt about precedence or type coercion, add parentheses and explicit casts. Rust’s compiler will thank you by catching real problems early, and the next person reading your code will understand your intent without guesswork.