Menu

Sobrecarga de operadores em C++: operadores +, == e << personalizados

A sobrecarga de operadores em C++ permite que seus próprios tipos funcionem com operadores nativos como +, == e <<. Aprenda as regras de funções membro x não membro, como sobrecarregar os operadores de comparação e de fluxo, e as pegadinhas envolvendo tipos de retorno e o operador de atribuição.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Faça seus tipos parecerem nativos

Você já sabe que std::string permite escrever a + b para concatenar e cout << s para imprimir. Isso não são truques especiais do compilador: são funções comuns com nomes curiosos. A sobrecarga de operadores é o recurso que permite que suas classes se conectem à mesma sintaxe, para que um tipo Vector2 ou Money possa ser somado, comparado e impresso exatamente como um int.

O mecanismo é simples quando você o enxerga: uma expressão como a + b é uma abreviação. O compilador a reescreve como uma chamada a uma função chamada operator+ e procura uma que combine com os tipos dos operandos. Defina essa função para a sua classe e a + b passa a funcionar de repente. Na verdade, isso é uma forma especializada de sobrecarga de funções: valem as mesmas regras de resolução de nomes, só que com nomes em formato de operador.

Note que a função recebe os dois operandos por const&: a aritmética não deve modificar suas entradas, e referências evitam cópias. Ela retorna um novo Vector2 por valor: p + q precisa produzir um resultado novo sem tocar em p nem q, assim como 2 + 3 não altera o 2.

Membro x não membro

Há dois lugares para definir um operador: como membro da classe ou como função livre (não membro). Como membro, o operando à esquerda é o this implícito, então um operador binário recebe apenas um parâmetro explícito:

O const depois da lista de parâmetros importa: a + b não deve modificar a, então o membro é marcado como const. Use a forma membro para operadores que estão intrinsecamente ligados ao operando à esquerda e não precisam de conversões sobre ele: +=, [], (), -> e operadores unários como -x ou ++x.

A pegadinha dos membros: o operando à esquerda não pode ser convertido. Com o operator+ membro acima, a + 50 funciona (50 é convertido para Money no lado direito), mas 50 + a não compila: o operando à esquerda 50 é um int, e você não pode adicionar uma função membro a int. Um operador não membro resolve isso porque ambos os operandos são parâmetros explícitos e ambos podem ser convertidos:

Regra prática: faça operadores binários simétricos (+, ==, *) serem não membros para que as conversões funcionem dos dois lados; faça operadores que precisam modificar o operando à esquerda ou que estão ligados a ele (+=, [], =) serem membros.

Sobrecarregando o operador de fluxo

O operador mais comumente sobrecarregado é, de longe, o << para impressão. Você não pode torná-lo membro da sua classe, porque o operando à esquerda é um std::ostream (como cout), não o seu tipo - e você não é dono de ostream. Por isso ele é sempre um não membro que recebe o fluxo por referência não constante e o retorna:

Dois detalhes fazem isso funcionar. O fluxo é passado e retornado por referência (ostream&): fluxos não podem ser copiados, e retornar o mesmo fluxo é o que permite encadear cout << "p = " << p << "\n". Cada << retorna o fluxo para que o próximo << tenha a que se ligar. Esqueça o return os; e o encadeamento quebra.

Operadores de comparação

Para comparar seus objetos com ==, < e afins, sobrecarregue os operadores de comparação. Antes do C++20 você escrevia cada um na mão; a pegadinha principal é que operator< deve retornar um bool e definir uma ordenação consistente:

Escrever todas as seis comparações (==, !=, <, <=, >, >=) na mão é tedioso e propenso a erros. O C++20 adicionou o operador de comparação de três vias <=> (a "nave espacial"). Defini-lo por padrão junto com == gera todas as comparações para você:

= default diz ao compilador para comparar os membros na ordem de declaração, que é exatamente a ordenação lexicográfica que você escreveria na mão. Prefira isso em compiladores modernos.

O operador de atribuição e suas armadilhas

operator= (atribuição por cópia) é especial: o compilador gera um para você, e para classes simples esse padrão está correto. Você só precisa escrever o seu quando a classe gerencia um recurso - memória crua, um handle de arquivo - em que uma cópia membro a membro estaria errada. A assinatura canônica retorna *this por referência para que as atribuições possam encadear (a = b = c):

Duas armadilhas vivem nessa função curta. Primeiro, a verificação de autoatribuição if (this == &other): sem ela, a = a faria delete[] data e depois leria de other.data, que acabou de ser liberado - comportamento indefinido. Segundo, a ordem importa: numa versão feita à mão você não deve apagar o buffer antigo antes de ter copiado com segurança o novo (uma implementação real muitas vezes aloca primeiro, ou usa o idiom copy-and-swap, de modo que uma alocação que falha deixa o objeto intacto).

Uma pegadinha mais ampla: não sobrecarregue operadores de formas surpreendentes. Um operator+ que secretamente modifica seu operando à esquerda, ou um operator== que não é simétrico, confundirá todos os leitores e quebrará código da biblioteca padrão que assume os significados usuais. Sobrecarregue operadores apenas quando a operação for genuinamente "do tipo soma" ou "do tipo igualdade" para o seu tipo.

Próximo: Especificadores de acesso

Repare como todos os exemplos mantiveram seus membros de dados private e expuseram comportamento por meio de uma pequena superfície pública: construtores, operadores e alguns métodos. Essa fronteira entre o que é visível para o mundo externo e o que fica oculto dentro da classe é controlada pelos especificadores de acesso: public, private e protected. A seguir veremos exatamente o que cada um permite, por que dados private com métodos públicos são o padrão para um bom encapsulamento e como protected se encaixa na herança.

Perguntas frequentes

O que é sobrecarga de operadores em C++?

A sobrecarga de operadores permite definir o que operadores nativos como +, == ou << significam para os seus próprios tipos. Você escreve uma função com nome especial - operator+, operator==, etc. - e o compilador a chama sempre que o operador aparece com operandos da sua classe. É assim que string + string concatena e cout << obj imprime um objeto personalizado.

Os operadores devem ser funções membro ou não membro (friend) em C++?

Use uma função membro quando o operando à esquerda é a sua própria classe e não precisa de conversões (por exemplo, +=, [], ()). Use uma função não membro (geralmente friend) quando o operando à esquerda pode ser um tipo nativo ou quando você quer conversões simétricas dos dois lados; isso é obrigatório para operator<<, porque o operando à esquerda é um std::ostream, não a sua classe.

Quais operadores de C++ não podem ser sobrecarregados?

Você não pode sobrecarregar :: (resolução de escopo), . (acesso a membro), .* (acesso por ponteiro para membro), ?: (ternário) e sizeof. Também não pode inventar operadores totalmente novos nem alterar a aridade ou a precedência de um operador - + é sempre binário, com a mesma precedência, quer some int ou o seu Vector2.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR