Chegamos ao artigo que fecha a Fase 3. No artigo Contratos e Destruição Segura — Classes Abstratas e Destrutores Virtuais completamos a orientação a objetos com classes abstratas e destrutores virtuais. Agora damos aos nossos tipos um poder que, até aqui, só os tipos embutidos e alguns da biblioteca (como std::string) tinham: responder aos operadores da linguagem. Você já usou a + b com strings, c1 == c2 para comparar, std::cout << x para imprimir — e talvez tenha se perguntado como o + "sabe" concatenar strings. A resposta é a sobrecarga de operadores, que nada mais é do que a sobrecarga de funções do artigo Um Nome, Vários Sentidos — Sobrecarga de Funções aplicada aos símbolos. Hoje você aprende a fazer seus tipos somarem, compararem e se imprimirem naturalmente — com a honestidade de sempre sobre onde isso ilumina o código e onde vira abuso.
Operadores são funções com nome esquisito
A ideia-chave, que desmistifica tudo: em C++, um operador como + é apenas uma função com um nome especial, operator+. Quando você escreve a + b, o compilador procura uma função operator+ que aceite os tipos de a e b. Para tipos embutidos, ela é interna; para os seus, você a fornece. Vamos dar a uma classe Vetor2D a capacidade de somar:
#include <iostream>
class Vetor2D {
public:
Vetor2D(double x, double y) : x_(x), y_(y) {}
// operator+ como MÉTODO: 'this' é o operando à esquerda, 'outro' o à direita.
// Recebe por const& (não modifica, não copia) e devolve um novo Vetor2D.
Vetor2D operator+(const Vetor2D& outro) const {
return Vetor2D(x_ + outro.x_, y_ + outro.y_);
}
double x() const { return x_; }
double y() const { return y_; }
private:
double x_, y_;
};
int main() {
Vetor2D a(1.0, 2.0);
Vetor2D b(3.0, 4.0);
Vetor2D c = a + b; // chama a.operator+(b)
std::cout << c.x() << ", " << c.y() << '\n'; // 4, 6
return 0;
}
a + b é açúcar sintático para a.operator+(b). Note as decisões de projeto que já são reflexo seu: o parâmetro é const Vetor2D& (artigos Referências e Ponteiros, Frente a Frente, A Disciplina do const — Promessas que o Compilador Cobra), o método é const porque somar não altera o operando esquerdo (Projetando Abstrações), e retornamos um objeto novo por valor — que, graças a move semantics (Quando Copiar Dá Errado), é barato. Toda a bagagem do curso converge aqui.
Imprimir com <<: o operador que precisa ser função livre
Para fazer std::cout << meu_objeto funcionar, sobrecarregamos operator<<. Aqui há uma sutileza importante: o operando à esquerda é o stream (std::ostream), não o seu objeto. Portanto operator<< não pode ser um método da sua classe (isso o tornaria membro do seu tipo, com this à esquerda); precisa ser uma função livre:
#include <iostream>
class Vetor2D {
public:
Vetor2D(double x, double y) : x_(x), y_(y) {}
double x() const { return x_; }
double y() const { return y_; }
private:
double x_, y_;
};
// operator<< como função LIVRE: à esquerda o stream, à direita o objeto.
// Recebe o stream por referência (para modificá-lo) e o devolve, para encadear.
std::ostream& operator<<(std::ostream& os, const Vetor2D& v) {
os << "(" << v.x() << ", " << v.y() << ")";
return os; // devolver o stream permite: cout << v1 << v2 << '\n';
}
int main() {
Vetor2D v(3.0, 4.0);
std::cout << "vetor = " << v << '\n'; // vetor = (3, 4)
return 0;
}
Dois pontos: o retorno é std::ostream& para permitir encadear (cout << a << b), o mesmo truque do return *this que vimos em interfaces fluentes (Projetando Abstrações); e como aqui usei os getters públicos, operator<< não precisou ser friend. Quando o operador livre precisa acessar membros privados, declara-se friend dentro da classe — um mecanismo que concede acesso privado a uma função externa específica, útil e a ser usado com parcimônia.
Comparações e o presente do C++20
Comparar objetos com ==, <, etc., é outro uso natural. Antes do C++20, você escrevia cada operador de comparação à mão — seis funções (==, !=, <, <=, >, >=), tedioso e propenso a inconsistências. O C++20 trouxe uma dádiva: o operador de comparação de três vias <=> (apelidado "spaceship") e a capacidade de pedir os operadores com = default, gerando todos de uma vez de forma consistente:
#include <iostream>
#include <compare> // necessário para <=> com = default
class Versao {
public:
Versao(int maior, int menor) : maior_(maior), menor_(menor) {}
// Uma linha gera ==, !=, <, <=, >, >= de forma consistente (C++20).
// A comparação segue a ordem de declaração dos membros: primeiro maior_, depois menor_.
auto operator<=>(const Versao&) const = default;
bool operator==(const Versao&) const = default;
private:
int maior_, menor_;
};
int main() {
Versao v1(2, 5), v2(2, 8);
std::cout << (v1 < v2) << '\n'; // 1 (2.5 < 2.8)
std::cout << (v1 == v2) << '\n'; // 0
std::cout << (v1 != v2) << '\n'; // 1
return 0;
}
Isto é C++20 — sempre digo a versão. Antes dele, você escreveria pelo menos operator== e operator< à mão. O <=> = default compara os membros na ordem de declaração, exatamente a ordem que estudamos no artigo A Vida Começa Aqui — Inicialização de Membros a Fundo, agora servindo à comparação. É um dos recursos modernos que mais reduzem código repetitivo.
A honestidade: sobrecarregar com bom senso
Prometo sempre o trade-off, e com operadores ele é sobre legibilidade, não desempenho. A regra de ouro: sobrecarregue um operador apenas quando o significado for óbvio e natural. Somar dois vetores com + é claro. Concatenar strings com + é claro. Mas sobrecarregar + para "adicionar um funcionário a um departamento" é confuso — ninguém adivinha o que departamento + funcionario faz, e um método bem-nomeado como departamento.contrata(funcionario) comunica muito melhor. O critério: se um leitor que nunca viu seu código consegue prever o que o operador faz, sobrecarregue; se ele teria de consultar a documentação, use um método com nome. Operadores existem para tornar o óbvio conciso, não para tornar o obscuro enigmático.
Sobrecarregar operadores existe para que um tipo próprio se comporte como os embutidos — somar com +, comparar com ==, sair em std::cout com <<. O critério é a única parte difícil: o símbolo precisa significar no seu tipo o que significa em todo lugar, senão o código fica conciso e ilegível ao mesmo tempo. O << ilustra a mecânica com clareza — ele é função livre porque o operando da esquerda é o stream, não o seu objeto.
Com isto se encerra a orientação a objetos, e o ganho maior não é sintaxe: é juízo de projeto. Saber quando uma classe ajuda e quando atrapalha, quando herança é a ferramenta certa e quando composição resolve melhor, o que deve ser público e por quê. São decisões que a linguagem permite tomar de vários jeitos e cobra depois, quando o código precisa mudar.
Fontes e leituras recomendadas
- Bjarne Stroustrup, A Tour of C++ (3ª ed.), seção sobre operadores: a visão do criador sobre quando sobrecarregar.
- cppreference.com/w/cpp/language/operators: a referência técnica completa, listando quais operadores podem ser sobrecarregados e como.
- cppreference.com/w/cpp/language/default_comparisons: os detalhes do
operator<=>e das comparações padronizadas do C++20. - Scott Meyers, Effective C++ (3ª ed.), Item 23 ("Prefer non-member non-friend functions") e Item 24: orientações sobre operadores como membros vs funções livres.
- ISO C++ Core Guidelines, seção "C.over" (Overloading), regras C.160 ("Define operators primarily to mimic conventional usage") e C.161: as diretrizes sobre sobrecarregar com bom senso.
Exercícios
Exercício 1
Dê à classe Vetor2D da aula um operator- (subtração) e um operator* que multiplique o vetor por um escalar (double). Demonstre os dois.
Ver resposta
✓ Resposta: Subtração e multiplicação por escalar:
#include <iostream>
class Vetor2D {
public:
Vetor2D(double x, double y) : x_(x), y_(y) {}
Vetor2D operator-(const Vetor2D& o) const { return Vetor2D(x_ - o.x_, y_ - o.y_); }
Vetor2D operator*(double k) const { return Vetor2D(x_ * k, y_ * k); } // vetor * escalar
double x() const { return x_; }
double y() const { return y_; }
private:
double x_, y_;
};
int main() {
Vetor2D a(5, 6), b(1, 2);
Vetor2D d = a - b; // (4, 4)
Vetor2D e = a * 2.0; // (10, 12)
std::cout << d.x() << "," << d.y() << " " << e.x() << "," << e.y() << '\n';
return 0;
}
Exercício 2
Escreva uma classe Dinheiro (com centavos como int) e sobrecarregue operator+ e operator<< para que Dinheiro a + b e std::cout << a funcionem, imprimindo no formato "R$ X,YY".
Ver resposta
✓ Resposta: A classe Dinheiro:
#include <iostream>
class Dinheiro {
public:
Dinheiro(int centavos) : centavos_(centavos) {}
Dinheiro operator+(const Dinheiro& o) const { return Dinheiro(centavos_ + o.centavos_); }
int centavos() const { return centavos_; }
private:
int centavos_;
};
std::ostream& operator<<(std::ostream& os, const Dinheiro& d) {
os << "R$ " << d.centavos() / 100 << ","
<< (d.centavos() % 100 < 10 ? "0" : "") << d.centavos() % 100;
return os;
}
int main() {
Dinheiro a(1250), b(375); // R$ 12,50 e R$ 3,75
std::cout << a + b << '\n'; // R$ 16,25
return 0;
}
O << cuida de imprimir os centavos com zero à esquerda quando menores que 10.
Exercício 3
Explique por que operator<< para impressão precisa ser uma função livre e não pode ser um método da sua classe. O que estaria "à esquerda" se fosse método?
Ver resposta
✓ Resposta: operator<< precisa ser função livre porque o operando à esquerda de std::cout << obj é o stream (std::ostream), não o seu objeto. Se fosse um método da sua classe, o operando à esquerda seria o seu objeto (this), e a chamada teria de ser obj << std::cout — o inverso do que se escreve. Como você não pode adicionar métodos à classe std::ostream (ela não é sua), a única forma de ter o stream à esquerda é uma função livre operator<<(std::ostream&, const SeuTipo&).
Exercício 4
Usando C++20, dê à classe Dinheiro do exercício 2 os operadores de comparação com uma única linha = default. Depois explique, com base na ordem de membros, o que aconteceria se Dinheiro tivesse dois membros (reais e centavos) declarados nessa ordem.
Ver resposta
✓ Resposta: Com C++20:
#include <compare>
class Dinheiro {
public:
Dinheiro(int c) : centavos_(c) {}
auto operator<=>(const Dinheiro&) const = default;
bool operator==(const Dinheiro&) const = default;
private:
int centavos_;
};
Se Dinheiro tivesse dois membros int reais e int centavos declarados nessa ordem, o <=> padrão compararia primeiro reais, depois centavos — a ordem de declaração. Isso daria a comparação correta para dinheiro (R$ 5,00 > R$ 4,99 porque 5 > 4 no primeiro membro). Se a ordem de declaração fosse invertida (centavos antes de reais), a comparação ficaria errada, comparando centavos antes de reais — um lembrete de que a ordem de declaração dos membros tem consequências reais, como vimos no artigo A Vida Começa Aqui — Inicialização de Membros a Fundo.
Exercício 5
Critique o seguinte design: uma classe Playlist que sobrecarrega operator+ para adicionar uma música e operator- para removê-la. Isso é bom uso de sobrecarga? Proponha uma alternativa e justifique com a regra de ouro da aula.
Ver resposta
✓ Resposta: O design é ruim. playlist + musica e playlist - musica não têm significado óbvio: + universalmente sugere soma/combinação de coisas do mesmo tipo, não "inserir um elemento numa coleção"; um leitor não adivinha se - musica remove a primeira ocorrência, todas, ou por índice. Alternativa clara:
class Playlist {
public:
void adiciona(const Musica& m);
void remove(const Musica& m);
};
// playlist.adiciona(m); playlist.remove(m); — intenção explícita
Pela regra de ouro da aula, um operador só se justifica quando seu efeito é previsível sem consulta à documentação. adiciona/remove comunicam exatamente o que fazem; +/- sobre uma playlist exigiriam explicação — logo, métodos nomeados são superiores aqui. Operadores servem ao óbvio, não ao arbitrário.