A sobrecarga de operadores em Rust é uma funcionalidade que permite que operadores como +, *, [] e outros sejam redefinidos para tipos definidos pelo usuário. Diferente de linguagens como C++, Rust não permite a criação de novos operadores nem a sobrecarga arbitrária de qualquer operador; em vez disso, ela fornece um conjunto de traits (como Add, Mul, Index) que, quando implementados para um tipo, habilitam o uso dos operadores correspondentes. Essa abordagem garante consistência e previsibilidade, pois o significado dos operadores permanece ligado a traits bem definidos.

Nesta aula, vamos aprender a implementar os traits mais comuns de operadores aritméticos e de indexação. Veremos também como lidar com ownership e borrowing, já que os métodos dos traits recebem self por referência ou por valor, dependendo do operador. Por fim, exemplos práticos mostrarão como criar tipos como números complexos e matrizes que suportam operações matemáticas e acesso indexado.

Traits Add, Mul, etc.

Os operadores aritméticos em Rust são definidos por traits no módulo std::ops. Cada operador tem seu próprio trait: Add para +, Sub para -, Mul para *, Div para /, Rem para %, entre outros. Esses traits geralmente têm um tipo associado Output que define o tipo do resultado da operação. Por exemplo, o trait Add é definido como:

pub trait Add<RHS = Self> {
    type Output;
    fn add(self, rhs: RHS) -> Self::Output;
}

O parâmetro de tipo RHS (right-hand side) permite que o operando direito seja de um tipo diferente do esquerdo, com valor padrão igual a Self. Isso significa que podemos somar tipos diferentes, desde que implementemos o trait adequadamente. Por exemplo, podemos querer somar um Complex com um f64, ou um Vector com um escalar.

Além dos operadores binários, existem também os operadores unários como Neg (para o sinal negativo) e Not (para !). A implementação segue o mesmo padrão: implementar o trait correspondente com o método apropriado.

Index

O operador de indexação [] é representado pelos traits Index e IndexMut. O trait Index permite acesso somente leitura, enquanto IndexMut permite acesso mutável. Suas definições são:

pub trait Index<Idx> {
    type Output: ?Sized;
    fn index(&self, index: Idx) -> &Self::Output;
}

pub trait IndexMut<Idx>: Index<Idx> {
    fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}

O tipo Idx é o tipo do índice, que pode ser qualquer coisa: um usize, um intervalo (Range), uma tupla, etc. O tipo associado Output é o tipo do elemento retornado. Note que Index retorna uma referência (&Self::Output), e IndexMut retorna uma referência mutável. Isso permite que o operador [] seja usado tanto para leitura quanto para escrita, dependendo do contexto.

Um exemplo comum é implementar Index para uma matriz bidimensional, onde o índice pode ser uma tupla (usize, usize).

Implementando

Para implementar um trait de operador, devemos usar a sintaxe impl Trait for Tipo. Vamos considerar um tipo NumeroComplexo que representa números complexos (parte real e imaginária como f64). Queremos que ele suporte soma (+) e multiplicação (*) com outro NumeroComplexo.

use std::ops::{Add, Mul};

#[derive(Debug, Clone, Copy)]
struct NumeroComplexo {
    real: f64,
    imag: f64,
}

impl Add for NumeroComplexo {
    type Output = NumeroComplexo;

    fn add(self, other: NumeroComplexo) -> NumeroComplexo {
        NumeroComplexo {
            real: self.real + other.real,
            imag: self.imag + other.imag,
        }
    }
}

impl Mul for NumeroComplexo {
    type Output = NumeroComplexo;

    fn mul(self, other: NumeroComplexo) -> NumeroComplexo {
        NumeroComplexo {
            real: self.real * other.real - self.imag * other.imag,
            imag: self.real * other.imag + self.imag * other.real,
        }
    }
}

Agora, podemos usar os operadores + e * com instâncias de NumeroComplexo:

fn main() {
    let a = NumeroComplexo { real: 1.0, imag: 2.0 };
    let b = NumeroComplexo { real: 3.0, imag: 4.0 };
    let c = a + b;
    let d = a * b;
    println!("Soma: {:?}", c);
    println!("Produto: {:?}", d);
}

Para implementar Index, considere um tipo Matriz que armazena dados em um vetor unidimensional, mas queremos acessar elementos com índices bidimensionais (linha, coluna):

use std::ops::Index;

struct Matriz {
    linhas: usize,
    colunas: usize,
    dados: Vec<f64>,
}

impl Index<(usize, usize)> for Matriz {
    type Output = f64;

    fn index(&self, (linha, coluna): (usize, usize)) -> &f64 {
        &self.dados[linha * self.colunas + coluna]
    }
}

// Para permitir escrita, implementamos IndexMut
impl IndexMut<(usize, usize)> for Matriz {
    fn index_mut(&mut self, (linha, coluna): (usize, usize)) -> &mut f64 {
        &mut self.dados[linha * self.colunas + coluna]
    }
}

Com isso, podemos usar matriz[(0,0)] para acessar o elemento.

Exemplos

Vamos a um exemplo completo que utiliza sobrecarga de operadores para criar um tipo Ponto que suporte soma e multiplicação por escalar, e também indexação para acessar coordenadas x e y.

use std::ops::{Add, Mul, Index};

#[derive(Debug, Clone, Copy)]
struct Ponto {
    x: f64,
    y: f64,
}

// Soma de dois pontos
impl Add for Ponto {
    type Output = Ponto;

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

// Multiplicação por escalar (f64)
impl Mul<f64> for Ponto {
    type Output = Ponto;

    fn mul(self, escalar: f64) -> Ponto {
        Ponto {
            x: self.x * escalar,
            y: self.y * escalar,
        }
    }
}

// Indexação: 0 para x, 1 para y
impl Index<usize> for Ponto {
    type Output = f64;

    fn index(&self, idx: usize) -> &f64 {
        match idx {
            0 => &self.x,
            1 => &self.y,
            _ => panic!("Índice fora dos limites: {}", idx),
        }
    }
}

fn main() {
    let p1 = Ponto { x: 1.0, y: 2.0 };
    let p2 = Ponto { x: 3.0, y: 4.0 };
    let soma = p1 + p2;
    let escalado = p1 * 2.5;
    println!("Soma: {:?}", soma);
    println!("Escalado: {:?}", escalado);
    println!("Coordenada x de p1: {}", p1[0]);
    println!("Coordenada y de p2: {}", p2[1]);
}

Neste exemplo, note que a multiplicação é definida apenas para Ponto * f64. Se quisermos também f64 * Ponto, precisaríamos implementar Mul<Ponto> for f64 (ou usar uma abordagem com trait genérico). Além disso, a indexação com usize permite acesso às coordenadas como se fosse um array.

Boas práticas

Ao sobrecarregar operadores, mantenha a semântica intuitiva: o operador + deve representar adição ou concatenação, * multiplicação ou produto, etc. Evite surpresas. Sempre implemente o trait Add junto com AddAssign (para o operador +=) se fizer sentido, e o mesmo para outros operadores. Lembre-se de que os métodos consomem self por valor; se quiser evitar cópias, implemente para referências também (ex.: impl Add for &Ponto).

Referências

Exercícios

  1. Implemente o trait Sub para o tipo NumeroComplexo (subtração). Use a mesma estrutura da aula.

    ✓ Resposta:
    impl Sub for NumeroComplexo {
        type Output = NumeroComplexo;
    
        fn sub(self, other: NumeroComplexo) -> NumeroComplexo {
            NumeroComplexo {
                real: self.real - other.real,
                imag: self.imag - other.imag,
            }
        }
    }
  2. Implemente o trait Index para um tipo Vetor que armazena f64 em um Vec. O índice deve ser usize.

    ✓ Resposta:
    struct Vetor {
        dados: Vec<f64>,
    }
    
    impl Index<usize> for Vetor {
        type Output = f64;
    
        fn index(&self, idx: usize) -> &f64 {
            &self.dados[idx]
        }
    }
  3. Implemente o trait Add para o tipo Ponto de forma que ele também aceite um escalar f64 (some o escalar a ambas as coordenadas).

    ✓ Resposta:
    impl Add<f64> for Ponto {
        type Output = Ponto;
    
        fn add(self, escalar: f64) -> Ponto {
            Ponto {
                x: self.x + escalar,
                y: self.y + escalar,
            }
        }
    }
  4. Crie um tipo CorRGB com campos r, g, b (tipo u8) e implemente o trait Mul<f64> para escalonar as cores (multiplicar cada componente pelo escalar, truncando para u8).

    ✓ Resposta:
    struct CorRGB {
        r: u8,
        g: u8,
        b: u8,
    }
    
    impl Mul<f64> for CorRGB {
        type Output = CorRGB;
    
        fn mul(self, escalar: f64) -> CorRGB {
            CorRGB {
                r: (self.r as f64 * escalar).round().min(255.0) as u8,
                g: (self.g as f64 * escalar).round().min(255.0) as u8,
                b: (self.b as f64 * escalar).round().min(255.0) as u8,
            }
        }
    }
  5. Implemente o trait Neg para o tipo NumeroComplexo (operador unário -).

    ✓ Resposta:
    use std::ops::Neg;
    
    impl Neg for NumeroComplexo {
        type Output = NumeroComplexo;
    
        fn neg(self) -> NumeroComplexo {
            NumeroComplexo {
                real: -self.real,
                imag: -self.imag,
            }
        }
    }