C++ Sobrecarga de Operadores

Com números, você escreve preco + frete. Com std::string, escreve nome + " Silva", e com std::cout, std::cout << nome. Por que esses sinais funcionam com tipos tão diferentes? Porque alguém ensinou o + e o << a cada um deles. Nesta aula você vai fazer o mesmo com uma classe sua, Dinheiro: somar valores com +, comparar com == e <, imprimir com std::cout <<. E vai aprender a parte mais importante: quando não fazer isso.

Você vai aprender
  • O que é sobrecarregar um operador: escrever uma função chamada operator+.
  • operator+, operator* e operator+= numa classe Dinheiro.
  • operator<< para imprimir com std::cout, e por que ele é uma função livre.
  • operator== e o operator<=> do C++20 com = default, que dão todas as comparações de uma vez.
  • operator[], para acessar itens com colchetes.
  • Método × função livre, os erros reais do compilador e as regras de bom senso.

Antes, veja Sobrecarga de Funções (a mesma ideia, com funções comuns) e Métodos const. Os programas foram compilados com o g++ 15 na máquina da Ana, na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.


A ideia: o mesmo sinal, outro significado

Na escola, o + serve para somar números. Mas, numa receita, "farinha + ovos" quer dizer "junte os dois", e ninguém se confunde: o sentido vem do contexto. Em C++, cada operador (+, ==, <<, []…) é na verdade uma função com um nome especial. Quando você escreve:

a + b

o compilador procura uma função chamada operator+ que aceite a e b, e a chama. Para int, ela já existe. Para a sua classe, você escreve. Isso se chama sobrecarga de operadores (operator overloading): dar mais um significado a um operador que já existe.


1. O problema: a classe Dinheiro não sabe somar

Guardar dinheiro em double traz problemas de arredondamento (aula float e double). Um jeito profissional é guardar centavos num número inteiro. A classe começa assim:

#include <iostream>

class Dinheiro {
public:
    explicit Dinheiro(long long centavos = 0) : centavos_{centavos} {}
    long long centavos() const { return centavos_; }
private:
    long long centavos_;
};

int main() {
    Dinheiro caderno{1290};
    Dinheiro lapis{150};
    Dinheiro total = caderno + lapis;
    std::cout << total.centavos() << '\n';
}

O explicit no construtor impede conversões escondidas: sem ele, Dinheiro d = 5; compilaria (e seriam 5 centavos ou 5 reais?). Com ele, é preciso escrever Dinheiro{5}, deixando claro. Agora, o erro:

$ g++ -std=c++23 -Wall -Wextra op1.cpp -o op1
op1.cpp: In function ‘int main()’:
op1.cpp:14:30: error: no match for ‘operator+’ (operand types are ‘Dinheiro’ and ‘Dinheiro’)
   14 |     Dinheiro total = caderno + lapis;
      |                      ~~~~~~~ ^ ~~~~~
      |                      |         |
      |                      Dinheiro  Dinheiro

"Nenhuma correspondência para operator+ (os operandos são Dinheiro e Dinheiro)". O g++ até desenha setas mostrando o tipo de cada lado. O compilador procurou a função operator+ para dois Dinheiro e não achou. (Clang: invalid operands to binary expression ('Dinheiro' and 'Dinheiro').)


2. operator+: ensinando a somar

Escreva, dentro da classe, um método chamado operator+:

Dinheiro operator+(const Dinheiro& outro) const {
    return Dinheiro{centavos_ + outro.centavos_};
}
Dinheiro
O tipo do resultado: somar dois valores dá um valor novo.
operator+
O nome da função: a palavra operator colada no sinal.
(const Dinheiro& outro)
O que está à direita do +. O da esquerda é o próprio objeto (this).
const
Somar não muda nenhuma das parcelas, como em 2 + 3.
return Dinheiro{…};
Cria e devolve o resultado.

A partir daí, caderno + lapis é só um jeito bonito de escrever caderno.operator+(lapis). As duas formas funcionam e fazem exatamente a mesma coisa.

Outros operadores aritméticos

Do mesmo jeito, dá para ensinar * (preço vezes quantidade) e += (somar no próprio objeto):

Dinheiro operator*(int quantidade) const {
    return Dinheiro{centavos_ * quantidade};
}
Dinheiro& operator+=(const Dinheiro& outro) {
    centavos_ += outro.centavos_;
    return *this;
}

Repare nas diferenças: o * recebe um int (dinheiro × número, e não dinheiro × dinheiro, que não faz sentido); o += não é const, porque muda o objeto da esquerda, e devolve *this por referência, como o operator= da aula Cópia e Movimento.


3. operator<<: imprimir com std::cout

Tente imprimir um Dinheiro sem ensinar o <<:

$ g++ -std=c++23 -Wall -Wextra op2.cpp -o op2
op2.cpp: In function ‘int main()’:
op2.cpp:13:15: error: no match for ‘operator<<’ (operand types are ‘std::ostream’ {aka ‘std::basic_ostream<char>’} and ‘Dinheiro’)
   13 |     std::cout << caderno << '\n';
      |     ~~~~~~~~~ ^~ ~~~~~~~
      |          |       |
      |          |       Dinheiro
      |          std::ostream {aka std::basic_ostream<char>}
op2.cpp:13:15: note: there are 52 candidates
                         « (mais 370 linhas: uma explicação para cada um dos 52 candidatos)

É o mesmo erro da soma, só que muito mais longo: o g++ lista os 52 operator<< que existem (para int, double, char, bool…) e explica por que nenhum aceita um Dinheiro. Diante de uma parede de texto assim, leia só a primeira linha error:. Ela já diz tudo: falta um operator<< para std::ostream e Dinheiro.

O std::ostream ("output stream", fluxo de saída) é o tipo do std::cout. A função que falta é esta, escrita fora da classe:

std::ostream& operator<<(std::ostream& saida, const Dinheiro& d) {
    return saida << std::format("R$ {},{:02}", d.centavos() / 100, d.centavos() % 100);
}
std::ostream& saida
O lado esquerdo do <<: o std::cout (ou um arquivo, aula Arquivos). Por referência, porque fluxos não podem ser copiados.
const Dinheiro& d
O lado direito: o valor a imprimir.
std::format("R$ {},{:02}", …)
Monta o texto: os reais, uma vírgula e os centavos com 2 dígitos ({:02} completa com zero: 5 vira 05). Veja std::format.
return saida << …;
Devolve o próprio fluxo. É isso que permite encadear: std::cout << a << b << '\n' vira (std::cout << a) << b, e o resultado do primeiro << precisa ser o std::cout de novo.
Por que fora da classe?

Num método, o lado esquerdo do operador é sempre o próprio objeto. Em std::cout << caderno, o lado esquerdo é o std::cout, que não é um Dinheiro. Então o operator<< tem que ser uma função livre, com dois parâmetros. Se ela precisar ler atributos privados, a classe pode declará-la como friend (aula friend); aqui, o método público centavos() basta.


4. Comparações: == e <=> C++20

Para comparar dois valores, você poderia escrever seis funções: ==, !=, <, <=, >, >=. Era assim até o C++17. Desde o C++20, duas linhas bastam:

bool operator==(const Dinheiro&) const = default;
auto operator<=>(const Dinheiro&) const = default;
operator==(…) const = default
"Iguais quando todos os atributos são iguais". O compilador escreve o corpo. E o != vem de graça.
operator<=>
O operador nave espacial (spaceship), pelo formato <=>. Ele compara de uma vez e responde "menor", "igual" ou "maior". A partir dele, o compilador cria <, <=, > e >=.
auto … = default
Compara atributo por atributo, na ordem em que foram declarados. O auto deixa o compilador escolher o tipo da resposta. Precisa de #include <compare>.

O programa completo

#include <compare>
#include <format>
#include <iostream>

class Dinheiro {
public:
    explicit Dinheiro(long long centavos = 0) : centavos_{centavos} {}

    long long centavos() const { return centavos_; }

    Dinheiro operator+(const Dinheiro& outro) const {
        return Dinheiro{centavos_ + outro.centavos_};
    }
    Dinheiro operator*(int quantidade) const {
        return Dinheiro{centavos_ * quantidade};
    }
    Dinheiro& operator+=(const Dinheiro& outro) {
        centavos_ += outro.centavos_;
        return *this;
    }
    bool operator==(const Dinheiro&) const = default;
    auto operator<=>(const Dinheiro&) const = default;
private:
    long long centavos_;
};

std::ostream& operator<<(std::ostream& saida, const Dinheiro& d) {
    return saida << std::format("R$ {},{:02}", d.centavos() / 100, d.centavos() % 100);
}

int main() {
    Dinheiro caderno{1290};   // R$ 12,90
    Dinheiro lapis{150};      // R$ 1,50

    Dinheiro total = caderno + lapis * 3;
    std::cout << "Caderno: " << caderno << '\n';
    std::cout << "3 lápis: " << lapis * 3 << '\n';
    std::cout << "Total: " << total << '\n';

    Dinheiro carteira{2000};
    carteira += Dinheiro{500};
    std::cout << "Na carteira: " << carteira << '\n';

    std::cout << std::boolalpha;
    std::cout << "total == 17,40? " << (total == Dinheiro{1740}) << '\n';
    std::cout << "dá para pagar? " << (carteira >= total) << '\n';
    std::cout << "caderno < lápis? " << (caderno < lapis) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra dinheiro.cpp -o dinheiro
$ ./dinheiro
Caderno: R$ 12,90
3 lápis: R$ 4,50
Total: R$ 17,40          « caderno + lapis * 3: o * vem antes do +, como na matemática
Na carteira: R$ 25,00
total == 17,40? true
dá para pagar? true      « o >= veio do <=>
caderno < lápis? false

Observe que a precedência continua a mesma dos números: em caderno + lapis * 3, a multiplicação aconteceu primeiro. Você pode ensinar um operador a fazer outra coisa, mas não pode mudar a ordem em que ele é calculado (aula Precedência). O std::boolalpha faz o cout mostrar true/false em vez de 1/0.

Experimente: mude os preços e a quantidade de lápis. Tente std::cout << (caderno + caderno); e std::cout << (lapis != caderno);. Depois apague a linha do operator<=> e veja qual linha para de compilar.

#include <compare>
#include <format>
#include <iostream>

class Dinheiro {
public:
    explicit Dinheiro(long long centavos = 0) : centavos_{centavos} {}

    long long centavos() const { return centavos_; }

    Dinheiro operator+(const Dinheiro& outro) const {
        return Dinheiro{centavos_ + outro.centavos_};
    }
    Dinheiro operator*(int quantidade) const {
        return Dinheiro{centavos_ * quantidade};
    }
    bool operator==(const Dinheiro&) const = default;
    auto operator<=>(const Dinheiro&) const = default;
private:
    long long centavos_;
};

std::ostream& operator<<(std::ostream& saida, const Dinheiro& d) {
    return saida << std::format("R$ {},{:02}", d.centavos() / 100, d.centavos() % 100);
}

int main() {
    Dinheiro caderno{1290};
    Dinheiro lapis{150};
    Dinheiro total = caderno + lapis * 3;
    std::cout << "Total: " << total << '\n';
    std::cout << std::boolalpha << "caderno > lápis? " << (caderno > lapis) << '\n';
}

5. operator[]: colchetes na sua classe

Com std::vector, você escreve notas[0]. Para uma classe sua dar o mesmo acesso, sobrecarregue operator[]. Ele quase sempre vem em dupla: uma versão para objetos que podem mudar e outra para objetos const:

#include <cstddef>
#include <iostream>
#include <string>
#include <vector>

class Boletim {
public:
    Boletim(std::string aluno) : aluno_{aluno} {}

    void adicionar(double nota) { notas_.push_back(nota); }
    std::size_t quantidade() const { return notas_.size(); }

    double& operator[](std::size_t i) { return notas_[i]; }
    const double& operator[](std::size_t i) const { return notas_[i]; }
private:
    std::string aluno_;
    std::vector<double> notas_;
};

void imprimir(const Boletim& b) {
    for (std::size_t i = 0; i < b.quantidade(); ++i) {
        std::cout << "Nota " << i + 1 << ": " << b[i] << '\n';
    }
}

int main() {
    Boletim ana{"Ana"};
    ana.adicionar(7.5);
    ana.adicionar(8);
    ana.adicionar(6);

    std::cout << "Primeira nota: " << ana[0] << '\n';
    ana[2] = 9.5;                 // corrigiu a prova
    imprimir(ana);
}
VersãoDevolveUsada em
double& operator[](std::size_t i)Uma referência à nota: dá para mudarana[2] = 9.5;
const double& operator[](std::size_t i) constUma referência só de leiturab[i] dentro de imprimir, onde b é const Boletim&
$ g++ -std=c++23 -Wall -Wextra colchetes.cpp -o colchetes
$ ./colchetes
Primeira nota: 7.5
Nota 1: 7.5
Nota 2: 8
Nota 3: 9.5              « o ana[2] = 9.5 mudou a nota de verdade

Se o operator[] devolvesse double (sem &), ana[2] = 9.5 mudaria uma cópia, que seria jogada fora. Como no std::vector, este [] não confere se o índice existe: ana[10] é comportamento indefinido. C++23 O operator[] pode até receber vários índices, como tabela[linha, coluna].


6. Método ou função livre?

O operator* do Dinheiro é um método, então o lado esquerdo tem que ser um Dinheiro. lapis * 3 funciona; 3 * lapis, não:

$ g++ -std=c++23 -Wall -Wextra op4.cpp -o op4
op4.cpp: In function ‘int main()’:
op4.cpp:17:20: error: no match for ‘operator*’ (operand types are ‘int’ and ‘Dinheiro’)
   17 |     Dinheiro b = 3 * lapis;
      |                  ~ ^ ~~~~~
      |                  |   |
      |                  int Dinheiro

Para aceitar os dois lados, escreva os operadores como funções livres, fora da classe:

Dinheiro operator*(const Dinheiro& d, int quantidade) {
    return Dinheiro{d.centavos() * quantidade};
}

Dinheiro operator*(int quantidade, const Dinheiro& d) {
    return d * quantidade;       // reaproveita a de cima
}
$ g++ -std=c++23 -Wall -Wextra livre.cpp -o livre
$ ./livre
450 450                  « lapis * 3 e 3 * lapis
OperadorCostuma serPor quê
=, [], (), ->Método (obrigatório)A linguagem exige.
+=, -=, *=…MétodoMudam o objeto da esquerda.
<< e >> com fluxosFunção livre (obrigatório)O lado esquerdo é o fluxo, não a sua classe.
+, -, *, /Função livre ou métodoLivre, se os dois lados podem ser de tipos diferentes.
==, <=>Método com = defaultO C++20 já resolve os dois lados sozinho.

7. Regras e bom senso

O que a linguagem não deixa fazer:

  • Criar operadores novos. Não existe operator** nem operator<>: só os que o C++ já tem.
  • Mudar a precedência ou a quantidade de operandos. * continua antes de +.
  • Mudar o que acontece com tipos básicos. 1 + 1 é sempre 2: pelo menos um dos lados tem que ser uma classe sua.
  • Sobrecarregar ::, ., .*, ?: e sizeof.

E o que ela deixa, mas não deveria:

Quando não sobrecarregar
  • Quando o significado não é óbvio. cliente + produto quer dizer "o cliente comprou o produto"? Um método cliente.comprar(produto) diz isso sem ninguém precisar adivinhar.
  • Quando o operador faria algo diferente do "normal". Um + que muda as parcelas, um == que imprime algo, um - que soma: tudo compila, e tudo é uma armadilha para quem lê.
  • Pela metade. Se a classe tem +, as pessoas vão esperar +=; se tem <, vão esperar > (por isso o <=> é tão bom).

A regra de ouro: faça como os int fazem. Sobrecarregue quando a sua classe é "um tipo de número ou de coleção" (dinheiro, frações, vetores da física, matrizes, datas) e o operador tem o sentido de sempre.


Quando dá errado

"no match for ‘operator+’ (operand types are …)"

O que significa: não existe um operator+ para aquela combinação de tipos. Leia os tipos nos parênteses: ‘Dinheiro’ and ‘Dinheiro’ (falta o operador), ou ‘int’ and ‘Dinheiro’ (ele existe, mas como método, só com o Dinheiro à esquerda). (Clang: invalid operands to binary expression.)

Como consertar: escreva o operador para esses tipos; para o tipo básico à esquerda, use uma função livre.

"must have exactly one argument" no operator<<
class Dinheiro {
public:
    // ...
    std::ostream& operator<<(std::ostream& saida, const Dinheiro& d) {
        return saida << std::format("R$ {},{:02}", d.centavos_ / 100, d.centavos_ % 100);
    }
    // ...
};
$ g++ -std=c++23 -Wall -Wextra op3.cpp -o op3
op3.cpp:9:19: error: ‘std::ostream& Dinheiro::operator<<(std::ostream&, const Dinheiro&)’ must have exactly one argument
    9 |     std::ostream& operator<<(std::ostream& saida, const Dinheiro& d) {
      |                   ^~~~~~~~
op3.cpp: In function ‘int main()’:
op3.cpp:18:15: error: no match for ‘operator<<’ (operand types are ‘std::ostream’ {aka ‘std::basic_ostream<char>’} and ‘Dinheiro’)
                         « (seguem as 52 notas de candidatos)

O que significa: o operator<< foi escrito dentro da classe. Como método, ele já tem o objeto como lado esquerdo, então só caberia mais um parâmetro ("deve ter exatamente um argumento"). Com dois, ele é inválido, e o cout << caderno continua sem operador. (Clang: overloaded 'operator<<' must be a binary operator (has 3 parameters).)

Como consertar: mova a função para fora da classe e use métodos públicos; ou mantenha dentro, mas com a palavra friend na frente, que a transforma em função livre (aula friend).

a = b + c mudou o b

O que significa: o operator+ altera o próprio objeto (centavos_ += …) em vez de criar um resultado novo. Isso é o trabalho do +=.

Como consertar: marque o operator+ como const: aí o compilador recusa qualquer mudança nos atributos dentro dele.


Missão

Missão: a Ana quer calcular o troco. (1) Escreva o operator- do Dinheiro (parecido com o +). (2) Acrescente o operator<=> com = default, para o >= funcionar. (3) Tire as barras // do main. A saída deve ser Troco: R$ 2,60.

#include <compare>
#include <format>
#include <iostream>

class Dinheiro {
public:
    explicit Dinheiro(long long centavos = 0) : centavos_{centavos} {}
    long long centavos() const { return centavos_; }

    Dinheiro operator+(const Dinheiro& outro) const {
        return Dinheiro{centavos_ + outro.centavos_};
    }
    bool operator==(const Dinheiro&) const = default;
private:
    long long centavos_;
};

std::ostream& operator<<(std::ostream& saida, const Dinheiro& d) {
    return saida << std::format("R$ {},{:02}", d.centavos() / 100, d.centavos() % 100);
}

int main() {
    Dinheiro total{1740};
    Dinheiro pago{2000};
    std::cout << "Total: " << total << '\n';
    // if (pago >= total) {
    //     std::cout << "Troco: " << (pago - total) << '\n';
    // }
}
#include <compare>
#include <format>
#include <iostream>

class Dinheiro {
public:
    explicit Dinheiro(long long centavos = 0) : centavos_{centavos} {}
    long long centavos() const { return centavos_; }

    Dinheiro operator+(const Dinheiro& outro) const {
        return Dinheiro{centavos_ + outro.centavos_};
    }
    Dinheiro operator-(const Dinheiro& outro) const {
        return Dinheiro{centavos_ - outro.centavos_};
    }
    bool operator==(const Dinheiro&) const = default;
    auto operator<=>(const Dinheiro&) const = default;
private:
    long long centavos_;
};

std::ostream& operator<<(std::ostream& saida, const Dinheiro& d) {
    return saida << std::format("R$ {},{:02}", d.centavos() / 100, d.centavos() % 100);
}

int main() {
    Dinheiro total{1740};
    Dinheiro pago{2000};
    std::cout << "Total: " << total << '\n';
    if (pago >= total) {
        std::cout << "Troco: " << (pago - total) << '\n';
    }
}

A saída da solução no laboratório:

$ ./troco
Total: R$ 17,40
Troco: R$ 2,60

Referência

OperadorAssinatura típica
a + b (método)T operator+(const T& outro) const;
a + b (livre)T operator+(const T& a, const T& b);
a += bT& operator+=(const T& outro); (devolve *this)
a == bbool operator==(const T&) const = default; C++20
< <= > >=auto operator<=>(const T&) const = default; C++20
std::cout << astd::ostream& operator<<(std::ostream& s, const T& a); (livre)
a[i]X& operator[](std::size_t i); e const X& operator[](std::size_t i) const;
Não sobrecarregáveis:: . .* ?: sizeof

Exercícios

1. O nome da função

Qual é o nome da função que o compilador procura quando você escreve a - b?

2. Todas as comparações

Complete a linha que dá <, <=, > e >= a uma classe Fracao:

auto (const Fracao&) const = default;

3. O tipo do cout

Complete o primeiro parâmetro do operador que imprime um Ponto:

std::ostream& operator<<(, const Ponto& p)

4. Por que o operator<< para imprimir com cout não pode ser um método comum da sua classe?

5. Qual destas sobrecargas é uma má ideia?

6. O operator[] de uma classe devolve double (sem &). O que acontece com obj[0] = 5;?


Resumo

  • Cada operador é uma função chamada operator + sinal. a + b vira a.operator+(b) ou operator+(a, b).
  • +, -, * devolvem um objeto novo e são const; += muda o objeto e devolve *this.
  • operator<< para fluxos é uma função livre: std::ostream& operator<<(std::ostream&, const T&), devolvendo o fluxo.
  • C++20: operator== e operator<=> com = default dão as seis comparações.
  • operator[] vem em dupla (normal e const) e devolve referência.
  • Função livre quando o lado esquerdo pode não ser a sua classe (3 * lapis, cout << x).
  • Não dá para criar operadores nem mudar a precedência. Sobrecarregue só quando o sentido é o de sempre: "faça como os int fazem".