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.
- O que é sobrecarregar um operador: escrever uma função chamada
operator+. operator+,operator*eoperator+=numa classeDinheiro.operator<<para imprimir comstd::cout, e por que ele é uma função livre.operator==e ooperator<=>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
operatorcolada 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
<<: ostd::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:5vira05). 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 ostd::coutde novo.
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
autodeixa 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ão | Devolve | Usada em |
|---|---|---|
double& operator[](std::size_t i) | Uma referência à nota: dá para mudar | ana[2] = 9.5; |
const double& operator[](std::size_t i) const | Uma referência só de leitura | b[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
| Operador | Costuma ser | Por quê |
|---|---|---|
=, [], (), -> | Método (obrigatório) | A linguagem exige. |
+=, -=, *=… | Método | Mudam o objeto da esquerda. |
<< e >> com fluxos | Função livre (obrigatório) | O lado esquerdo é o fluxo, não a sua classe. |
+, -, *, / | Função livre ou método | Livre, se os dois lados podem ser de tipos diferentes. |
==, <=> | Método com = default | O 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**nemoperator<>: 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é sempre2: pelo menos um dos lados tem que ser uma classe sua. - Sobrecarregar
::,.,.*,?:esizeof.
E o que ela deixa, mas não deveria:
- Quando o significado não é óbvio.
cliente + produtoquer dizer "o cliente comprou o produto"? Um métodocliente.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
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.
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
| Operador | Assinatura típica |
|---|---|
a + b (método) | T operator+(const T& outro) const; |
a + b (livre) | T operator+(const T& a, const T& b); |
a += b | T& operator+=(const T& outro); (devolve *this) |
a == b | bool operator==(const T&) const = default; C++20 |
< <= > >= | auto operator<=>(const T&) const = default; C++20 |
std::cout << a | std::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:
3. O tipo do cout
Complete o primeiro parâmetro do operador que imprime um Ponto:
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 + bviraa.operator+(b)ouoperator+(a, b). +,-,*devolvem um objeto novo e sãoconst;+=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==eoperator<=>com= defaultdão as seis comparações. operator[]vem em dupla (normal econst) 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
intfazem".