Unary Operators

Overloading the unary negation and logical not operators in Rust using the Neg and Not traits from std::ops.

When you write -x or !flag in Rust, the compiler silently translates those expressions into method calls: x.neg() and flag.not(). Overloading the behaviour of these unary operators means implementing the corresponding traits on your own types. The two traits covered here are Neg for the minus sign and Not for the exclamation mark.

What Unary Operators Can Be Overloaded

Rust only allows overloading a handful of operators, each backed by a specific trait. For unary operators the story is compact:

  • The negation operator - maps to the Neg trait.
  • The logical‑not operator ! maps to the Not trait.

Other unary operators like the dereference * also have associated traits (Deref and DerefMut), but those are covered separately as they involve pointer‑like semantics rather than value transformation. This page concentrates on Neg and Not, which produce a new output from a single input value.

Prelude imports:

Both Neg and Not are in the standard prelude. You can implement them without adding an explicit use statement, though many codebases still include use std::ops::Neg; or use std::ops::Not; for clarity.

The Neg Trait

To enable the unary minus for a type, you implement std::ops::Neg. The trait has one required item: an associated type Output and a method fn neg(self) -> Self::Output. The compiler will call neg() whenever it encounters -value.

Consider a 2D vector where negation should flip both coordinates:

use std::ops::Neg;
#[derive(Debug, PartialEq)]
struct Vector2D {
    x: f64,
    y: f64,
}
impl Neg for Vector2D {
    type Output = Vector2D;
    fn neg(self) -> Vector2D {
        Vector2D {
            x: -self.x,
            y: -self.y,
        }
    }
}
fn main() {
    let v = Vector2D { x: 3.0, y: -4.0 };
    let negated = -v;
    assert_eq!(negated, Vector2D { x: -3.0, y: 4.0 });
}

The implementation consumes self (it takes ownership), so after -v the original v is no longer usable. For a type that implements Copy, like a small geometry struct with Copy derived, this is fine – v gets copied before being moved into neg. For non‑Copy types you usually want to implement Neg for a reference instead. The next section shows that pattern.

Check your output:

If the code compiles and the assertion passes, you’ve successfully overloaded - for your type. The operator now feels like a built‑in part of the language.

The Not Trait

The logical negation operator ! is overloaded through the Not trait, defined as:

pub trait Not {
    type Output;
    fn not(self) -> Self::Output;
}

It is typically used for boolean‑like types – custom wrappers around bool, bitmasks, or enums with a clear “opposite” variant. For instance, a newtype that toggles an active/inactive state:

use std::ops::Not;
#[derive(Debug, PartialEq)]
struct Active(bool);
impl Not for Active {
    type Output = Active;
    fn not(self) -> Active {
        Active(!self.0)
    }
}
fn main() {
    let on = Active(true);
    let off = !on;
    assert_eq!(off, Active(false));
}

Because Active wraps a bool, the implementation simply delegates to bool’s own Not implementation. The operation is expected to be a true logical inversion.

Keep logical not logical:

Overloading ! to perform something other than a logical inversion – flipping a geometric shape along an axis, for example – can surprise readers. While technically allowed, it violates the principle of least astonishment. Prefer a named method (flip_x()) unless your type truly represents a boolean‑like concept.

Working with References

The default trait signatures take self by value, which moves the operand. If you want to keep the original after applying the operator, you can implement the trait for references. For Neg, this looks like:

impl Neg for &Vector2D {
    type Output = Vector2D;
    fn neg(self) -> Vector2D {
        Vector2D {
            x: -self.x,
            y: -self.y,
        }
    }
}

Now you can write let neg = -&v; and v remains usable. This pattern is common in generic code where moves are undesirable.

Beware of accidental moves:

If you only implement Neg for the value type and then try -&v, the compiler will complain that Neg is not implemented for &Vector2D. The error message often suggests using a reference or adding a Copy bound. Don’t assume the reference version will just work – you must write it explicitly.

How Beginners Should Think About Overloading

Operators are just methods with a shorter name. When you see -a, mentally expand it to a.neg(). This mental model helps in two ways: you immediately know which trait is involved, and you realise that the operand is moved (or borrowed) just like any other method call. Before overloading, ask whether the operation feels natural as a negation or logical not. If the answer is “maybe, but not exactly”, define a regular method instead.

Common Mistakes and Misconceptions

Forgetting to specify the associated Output type
Every implementation of Neg or Not must declare type Output = ...;. If you omit it, the compiler error will mention that the associated type is missing, which is easy to fix but trips up newcomers.

Overloading ! for a type that doesn’t represent a Boolean concept
A struct FileDescriptor shouldn’t implement Not just to flip a flag. A future maintainer who reads !fd will have no idea what it does. Use a method like fd.set_nonblocking() instead.

Assuming operators are chainable without cloning
Because neg consumes self, chaining - -v won’t compile unless v is Copy or you’ve implemented Neg for references. If you need to apply the operator multiple times, implement for &T or derive Copy.

Mixing up Neg and Sub
-x (unary) is Neg; x - y (binary) is Sub. It’s tempting to think “minus is minus”, but they are separate traits. Implementing Neg does not give you subtraction, and vice versa.

Practical Real‑World Usage

  • Mathematical libraries: nalgebra and cgmath overload Neg for vectors and matrices so you can write -velocity to reverse direction.
  • Custom numeric wrappers: Types like Meters(f64) or Celsius(f64) often implement Neg to stay consistent with the underlying arithmetic.
  • Flag containers: A Permissions bitmask may implement Not to invert all bits in one go.
  • State machines: An enum Toggle { On, Off } can implement Not so that !state gives the opposite state – provided the mapping is obvious.

These use‑cases respect the original semantics of the operator, making the code more readable without introducing hidden behaviour.

Summary

Neg and Not give you a disciplined way to overload the unary - and ! operators. The key insight is that these operators are nothing more than method calls, and Rust’s trait system ensures the connection is explicit and type‑checked. When you stick to the natural meaning of the operators – numeric negation for Neg, logical inversion for Not – overloaded operators feel as clean as the built‑in ones.