A sobrecarga de operadores em C++ é um mecanismo que permite definir um comportamento diferenciado para um dos símbolos reservados da linguagem, como os aritméticos: +, -, ++ e –, ou os de comparação: ==, !=, <, >.
Com a sobrecarga de operadores em C++, podemos definir o que acontecerá quando escrevermos, por exemplo, a + b
sendo a e b objetos de uma classe criada por nós. Ou então, podemos sobrecarregar o operador<< para facilitar nossa vida na hora de imprimir na tela um objeto de uma classe (faço isso no exemplo 2).
Assim como a sobrecarga de funções, a sobrecarga de operadores é uma forma de polimorfismo estático.
Como fazer a sobrecarga de operadores em C++?
Para se sobrecarregar um operador em C++, é preciso escrever a palavra operator seguida do símbolo do operador em questão, e escrever como que o código para se definir uma função. Para se sobrecarregar o operador <<, por exemplo, eu preciso escrever o seguinte:
ostream& operator<<(ostream& ostr, const MinhaClasse& obj) { // Corpo da função }
Note que eu escrevo como se estivesse definindo uma função cujo nome é operator<< – o resto é tudo igual a uma função: um valor de retorno de tipo ostream&; dois parâmetros de entrada (ostream& e um objeto constante de uma classe criada pelo usuário, const MinhaClasse&), e o corpo da função. Isso é assim por uma razão muito simples: um operador é nada mais que uma função com um nome especial.
Note que o o operador << é diferente dos outros, pois que ele precisa retornar o tipo ostream& para se poder encadear prints como na expressão cout << "oi," << " tudo bem?" << endl
.
Passemos a um exemplo onde eu imprimo no terminal os dados de um objeto de um classe sem o uso da sobrecarga de operadores, para que se torne clara, então, a vantagem de sobrecarregar o operador << (e os operadores de modo geral).
Exemplo 1 – Imprimindo dados de uma classe sem sobrecarga de operadores
#include <iostream> using namespace std; class BankAccount { public: BankAccount(string owner, double balance) : _owner(move(owner)), _balance(balance) {} // Função para transferir a conta para outro nome inline void transferAccount(string newOwner) {_owner = move(newOwner);} inline string getOwner() const noexcept {return _owner;} inline double getBalance() const noexcept {return _balance;} private: string _owner; double _balance; }; int main() { BankAccount contaDePedro {"Pedro", 1000.0}; cout << "Account owner: " << contaDePedro.getOwner() << "\nAccount balance: " << contaDePedro.getBalance() << endl; return 0; }
No exemplo acima eu criei uma classe chamada BankAccount que representa uma conta bancária, e possui dois atributos privados: um string que guarda o nome do dono da conta, _owner, e um double que guarda o saldo da conta, _balance. Perceba que eu utilizo a notação que precede o nome de atributos privado de um _ (underscore).
A classe possui um construtor por parâmetros que recebe o nome do dono da conta e o saldo de abertura da conta; possui também uma função auxiliar para alterar o dono da conta, transferAccount, e duas funções getters que permitem recuperar os atributos privados do objeto, getOwner e getBalance. Cabe aqui dizer que eu omiti os setters (funções para modificar o valor dos atributos do objeto) porque eles não eram necessários ao nosso exemplo.
Precisávamos apenas ser capazes de construir objetos do tipo BankAccount e de acessar os seus atributos para imprimí-los no terminal, como eu fiz na linha 23 do exemplo 1. Para imprimir os dados do objeto contaDePedro, eu preciso usar getOwner e getBalance e construir o string que eu queria imprimir manualmente.
Talvez você tenha percebido que se eu tivesse 10 atributos na minha classe, e se eu quisesse imprimir os 10, eu teria que recuperar os 10 atributos e construir um string de saída gigantesco. Vou mais longe: e se eu quisesse imprimir os dados de 10 objetos diferentes? contaDeJoao, contaDeMarcos etc. Haja ctrl+c e ctrl+v! Esse processo é claramente tedioso e vunerável a erros humanos. Como podemos melhorá-lo?
Para facilitar a nossa vida, podemos sobrecarregar o operador << para imprimir objetos da classe BankAccount. Vejamos como isso funciona no exemplo 2.
Exemplo 2 – Sobrecarga do operador << em C++
#include <iostream> using namespace std; class BankAccount { public: BankAccount(string owner, double balance) : _owner(move(owner)), _balance(balance) {} // Função para transferir a conta para outro nome inline void transferAccount(string newOwner) {_owner = move(newOwner);} inline string getOwner() const noexcept {return _owner;} inline double getBalance() const noexcept {return _balance;} private: string _owner; double _balance; }; ostream& operator<<(ostream& ostr, const BankAccount& account) { ostr << "Account owner: " << account.getOwner() << "\nAccount balance: " << account.getBalance() << endl; return ostr; } int main() { BankAccount contaDePedro {"Pedro", 1000.0}; cout << contaDePedro; return 0; }
A saída do código acima é exatamente igual àquela do exemplo 1, mas o código da main é muito mais simples: para imprimir o objeto contaDePedro precisei escrever apenas cout << contaDePedro
. Isso é possível graças à sobrecarga do operador << que fiz na linha 21.
Perceba que eu sobrecarreguei o operador<< fora da classe BankAccount; ele não é uma função membro da classe, mas sim uma função definida no escopo global do programa que recebe um objeto do tipo BankAccount como parâmetro. Iss nos dá mais flexibilidade na hora de utilizar o operador<< no nosso código. Veremos o porquê na próxima seção.
O primeiro parâmetro de entrada do operator<< é uma referência a um objeto do tipo ostream, que serve para manipular fluxos de saída (lugares onde se pode escrever coisas desde o seu programa), como o cout, o cear e o clog.
No corpo da função utilizamos o parâmetro ostr para imprimir valores no terminal, em seguida retornamos uma referência ao próprio ostr, para que possamos encadear chamadas utilizando o operador <<, como em cout << "Hello" << ", World!" << endl
.
Sobrecarga de operadores no escopo global ou como métodos de uma classe
Talvez você tenha se perguntado por que eu não sobrecarreguei o operador<< como um método da classe BankAccount no exemplo 2. Eu poderia tê-lo feito, como fiz para o operador== no exemplo 4. Todavia, isso me traria alguns inconvenientes, como o do recorte de código a seguir
Exemplo 2.1 – Operador<< sobrecarregado como método de classe
#include <iostream> using namespace std; class BankAccount { public: // O resto da classe foi omitido para manter o recorte breve ostream& operator<<(ostream& ostr) const { ostr << "Account owner: " << this->getOwner() << "\nAccount balance: " << this->getBalance() << endl; return ostr; } // ... }; int main() { BankAccount contaDePedro {"Pedro", 1000.0}; contaDePedro << cout; return 0; }
No recorte acima eu modifiquei levemente o exemplo 2. Substitui a sobrecarga do operador<< como função global por uma sobrecarga do operador<< como método da classe BankAccount. O resultado disso foi que eu tive que inverter a ordem do cout
e da variável contaDePedro
para poder usar o operador<< (linha 19).
Eu fui obrigado a invertê-lo porque, como método da classe, o operador<< tem que ser invocado sobre um objeto da classe BankAccount; ou seja, o objeto precisa vir à sua esquerda, resultando na inversão da ordem normal com que se utiliza o cout.
Ok, até aí tudo bem, mas como eu sei qual tipo escolher, método ou função global, para sobrecarregar meus operadores? Vejamos a seguir as regras e recomendações.
Quando fazer sobrecarga de operadores como métodos
O primeiro ponto a se saber é que alguns operadores precisam ser sobrecarregados como métodos. Este é o caso do operador=, o operador de atribuição. Ele está tão intrinsicamente ligado à classe a que pertence que sobrecarregá-lo como uma função global não faria sentido.
Quando se trata de operadores que podem ser sobrecarregados como método ou como função global, há muita discussão na comunidade do C++, mas eu prefiro sobrecarregar operadores como métodos sempre que possível, a menos que o operador tenha que ser uma função global. Uma vantagem dessa abordagem é que métodos de classes podem marcados como virtual e sobrescritos em cadeias de heranças de classes.
Best practices
Sempre que sobrecarregar um operador em C++ como método de uma classe, marque o como const se ele não modificar o objeto sobre o qual é chamado, para que ele possa ser invocado com objetos constantes.
Quando fazer sobrecarga de operadores como funções globais
Sempre que se deseje utilizar um objeto de outro tipo que o da sua classe à esquerda do operador sobrecarregado, você deve sobrecarregá-lo como uma função global. Caso contrário, o código não compilará, pois o compilador espera sempre um objeto da classe à esquerda do operador para sobrecargas com métodos de classe.
Isso é verdade, por exemplo, para os operadores << (exemplo 2) e >>, onde há no lado esquerdo um objeto do tipo iostream (o cout ou o cin, por exemplo). Se usarmos a sobrecarga com método, teremos que fazer como mostrado na linha 19 do exemplo 2.1.
Na sobrecarga de operadores como funções globais, há sempre um parâmetro de entrada a mais que nos métodos, pois nos métodos o objeto sobre o qual se chama o operador já está representado implicitamente pelo ponteiro this.
Best practices
Sempre coloque operadores sobrecarregadores como funções globais dentro do mesmo namespace no qual a classe foi escrita (para evitar de poluir o namespace global do programa).
Sobrecarga de operadores de comparação em C++
Para utilizar as funcionalidade de comparação do C++, assim como fazemos com inteiros ou doubles, por exemplo, precisamos sobrecarregar os operadores ==, !=, <, >, <= e >= da nossa classe, ou seja, precisamos sobrecarregar 6 operadores. Felizmente, o grosso do trabalho é feito nos operadores == e < – os outros apenas reutilizam esses dois primeiros.
Vale notar que é recomendado fazer a sobrecarga de operadores de comparação como funções globais, para permitir mais flexibilidade no seu uso, como na seguinte expressão (que não compilaria com uma sobrecarga por método): if (10 < contaDePedro) {...}
.
Vejamos a seguir um exemplo onde sobrecarreguei os 6 operadores de comparação da classe BankAccount.
Exemplo 3 – Sobrecarga de operadores de comparação
#include <iostream> using namespace std; class BankAccount { public: BankAccount(string owner, double balance) : _owner(move(owner)), _balance(balance) {} // Função para transferir a conta para outro nome inline void transferAccount(string newOwner) {_owner = move(newOwner);} inline string getOwner() const noexcept {return _owner;} inline double getBalance() const noexcept {return _balance;} inline bool hasSameBalanceAs(const BankAccount& rhs) const { return this->getBalance() == rhs.getBalance(); } private: string _owner; double _balance; }; ostream& operator<<(ostream& ostr, const BankAccount& account) { ostr << "Account owner: " << account.getOwner() << "\nAccount balance: " << account.getBalance() << endl; return ostr; } // Sobrecarga dos operadores de comparação pré C++20 bool operator==(const BankAccount& lhs, const BankAccount& rhs) { return lhs.getOwner() == rhs.getOwner() && lhs.hasSameBalanceAs(rhs); } bool operator!=(const BankAccount& lhs, const BankAccount& rhs) { return !(lhs == rhs); } bool operator<(const BankAccount& lhs, const BankAccount& rhs) { return lhs.getBalance() < rhs.getBalance(); } bool operator>(const BankAccount& lhs, const BankAccount& rhs) { return !(lhs < rhs) && !lhs.hasSameBalanceAs(rhs); } bool operator<=(const BankAccount& lhs, const BankAccount& rhs) { return !(lhs > rhs); } bool operator>=(const BankAccount& lhs, const BankAccount& rhs) { return !(lhs < rhs); } int main() { BankAccount contaDePedro {"Pedro", 1000.0}; BankAccount contaDeMarcos {"Marcos", 1000.0}; BankAccount contaDeJoao {"Joao", 2000.0}; cout << contaDePedro; cout << boolalpha << "Conta de Pedro == Conta de Marcos: " << (contaDePedro == contaDeMarcos) << endl; cout << "Conta de Pedro < Conta de Joao: " << (contaDePedro < contaDeJoao); return 0; }
No exemplo 3, eu adicionei uma função auxiliar hasSameBalanceAs para comparar os saldos de duas contas bancárias, pois eu decidi utilizar o operator== (linha 30) para comparar o nome além do saldo (ou seja, comparar se a conta é idêntica à outra), tornando-o inadequado para verificar apenas uma igualdado em saldos de duas contas. Para o operator!= (linha 34), apenas inverti o retorno do operador==. Simples, não?
Para as demais comparações, começamos sempre pelo operator< (pois ele é o operador utilizado por grande parte dos algoritmos disponíveis na biblioteca padrão do C++), e construímos os demais a partir dele. Note que eu apenas estou interessado no saldo da conta, e não no nome do dono – acho que não faria muito sentido comparar qual nome é menor que o outro. Os demais operadores apenas usam uma combinação dos operadores < e da função hasSameBalanceAs, que funciona como um operator== apenas para o saldo.
Não foi muito difícil sobrecarregar os 6 operadores de comparação no exemplo 3, mas eu tive que escrever 6 funções, o que é um pouco tedioso e um procedimento no qual é fácil de se enganar em um pequeno detalhe. Para a nossa sorte, todavia, o C++20 introduziu um novo modo, muito mais simples, de sobrecarregar os 6 operadores de comparação: basta sobrecarregar os operadores == e <=>.
Sobrecarregando um único operador: <=>
Na verdade, se todos os atributos membros da sua classe forem de tipos que suportam o operador <=>, ou os operadores < e ==, todos os 6 operadores de comparação podem ser obtidos com uma única linha de código! Isso também pressupõe que você quer o comportamento padrão do operador ==, que é a comparação de todos os atributos da classe um por um – porque o compilador também escreve o operator== para nós à partir do operator<=>.
Se esse for o caso, uma única linha com o operador <=> sobrecarregado e marcado como =default fará o trabalho por você. No caso da nossa classe BankAccount, eu não queria o comportamento padrão do operator==, então eu não fiz isso, mas eu poderia ter escrito simplemente a linha abaixo (ao invés de sobrecarregar os operadores == e <=>, como fiz no exemplo 4).
[[nodiscard]] partial_ordering operator<=>(const BankAccount& hrs) const = default;
Sobrecarregando apenas dois operadores: == e <=>
Com a sobrecarga do operator==, em C++20, o operator!= é gerado automaticamente para nós pelo compilador, então não precisamos escrevê-lo (?). Lá se vão 2 dos 6.
Sobrecarregando o operador three-way, operator<=>, também conhecido como operador espaçonave ou nave espacial (do inglês spaceship), obtemos de uma tacada só os outros 4 operadores que nos restam: <, <=, >, >=. Esse novo operador condensa em si todas essas operações de comparação.
O procedimento para sobrecarregar o operator<=> é o mesmo que para os demais, afinal de contas o <=> é apenas um novo operador introduzido na linguagem. Contudo, note que o retorno do operador <=> é do tipo partial_ordering (para referência, consulte o artigo do CppReference, mas infelizmente não há versão em português para essa página), que serve para indicar se o resultado de uma comparação é menor, maior, equivalente ou indefinido.
No exemplo 4, linha 25, sobrecarreguei o operator<=> como um método da classe BankAccount. Se eu quisesse, também poderia ter substituído o tipo de retorno de partial_ordering para auto – o efeito seria o mesmo se todos os atributos da minha classe suportassem o operador <=> (o que é o caso para os tipos padrão). Ah, outra coisa: eu adicionei o decorador [[nodiscard]] ao retorno da função para que o compilador não permita que o usuário ignore o valor de retorno (é sempre uma boa idéia fazer isso).
Note ainda que no exemplo 4 eu utilizei a sobrecarga através de métodos, ao invés de funções globais, enquanto no exemplo 3 eu fiz o inverso. Por que? Outra coisa boa no C++20 é que o compilador consegue identificar quando utilizamos um operador de comparação da classe, mesmo que seja uma sobrecarga de método e que o objeto da classe esteja à direita do operador, e reescrever para nós o código para a ordem correta. Por trás dos panos, 10 == meuObj
torna-se meuObj == 10
.
Para reter:
Na sobrecarga de operadores em C++20, apenas o operator<=> marcado como =default basta para que o compilador gere todos os 6 operadores de comparação. No entanto, se você não quiser o comportamento padrão do operador ==, forneça também uma sobrecarga do operator==.
Ambos o operator<=> e o operator== podem ser sobrecarregados como métodos em C++20.
Exemplo 4 – Sobrecarga de operadores de comparação em C++20
#include <iostream> #include <compare> // partial_ordering vem daqui using namespace std; class BankAccount { public: BankAccount(string owner, double balance) : _owner(move(owner)), _balance(balance) {} // Função para transferir a conta para outro nome inline void transferAccount(string newOwner) {_owner = move(newOwner);} inline string getOwner() const noexcept {return _owner;} inline double getBalance() const noexcept {return _balance;} inline bool hasSameBalanceAs(const BankAccount& rhs) const { return this->getBalance() == rhs.getBalance(); } // Sobrecarga dos operadores == e <=>: só precisamos desses dois! bool operator==(const BankAccount& rhs) const { return this->getOwner() == rhs.getOwner() && this->hasSameBalanceAs(rhs); } [[nodiscard]] partial_ordering operator<=>(const BankAccount& rhs) const { return this->getBalance() <=> rhs.getBalance(); } private: string _owner; double _balance; }; ostream& operator<<(ostream& ostr, const BankAccount& account) { ostr << "Account owner: " << account.getOwner() << "\nAccount balance: " << account.getBalance() << endl; return ostr; } int main() { BankAccount contaDePedro {"Pedro", 1000.0}; BankAccount contaDeMarcos {"Marcos", 1000.0}; BankAccount contaDeJoao {"Joao", 2000.0}; cout << contaDePedro; cout << boolalpha << "Conta de Pedro == Conta de Marcos: " << (contaDePedro == contaDeMarcos) << endl; cout << "Saldo de Pedro <= Saldo de Marcos: " << (contaDePedro <= contaDeJoao) << endl; cout << "Saldo de Pedro < Saldo de Joao: " << (contaDePedro < contaDeJoao); return 0; }
Sobrecarga de operadores aritméticos em C++
Também é possível sobrecarregar os operadores aritméticos para que funcionem com objetos das nossas classes. Para isso, basta seguir o procedimento que utilizamos para a sobrecarga dos outros operadores, sabendo que é preferível fazer a sobrecarga de operadores aritméticos no formato de funções globais.
Vejamos no código do exemplo 5 a sobrecarga do operador + feita das duas formas, como método da classe BankAccount e também como função global.
Exemplo 5 – Sobrecarga do operador +
#include <iostream> #include <compare> using namespace std; class BankAccount { public: BankAccount(string owner, double balance) : _owner(move(owner)), _balance(balance) {} BankAccount(double balance) : _balance(balance) {} // Função para transferir a conta para outro nome inline void transferAccount(string newOwner) {_owner = move(newOwner);} inline string getOwner() const noexcept {return _owner;} inline double getBalance() const noexcept {return _balance;} inline bool hasSameBalanceAs(const BankAccount& rhs) const { return this->getBalance() == rhs.getBalance(); } // Sobrecarga dos operadores == e <=>: só precisamos desses dois! bool operator==(const BankAccount& rhs) const { return this->getOwner() == rhs.getOwner() && this->hasSameBalanceAs(rhs); } [[nodiscard]] partial_ordering operator<=>(const BankAccount& rhs) const { return this->getBalance() <=> rhs.getBalance(); } // Opção #1 - operador+ membro BankAccount operator+(const BankAccount& rhs) const { return BankAccount{this->getOwner() + " e " + rhs.getOwner(), this->getBalance() + rhs.getBalance()}; } private: string _owner {"Fantasma"}; double _balance; }; // Opção #2 - operador+ global (melhor) BankAccount operator+(const BankAccount& lhs, const BankAccount& rhs) { return BankAccount{lhs.getOwner() + " e " + rhs.getOwner(), lhs.getBalance() + rhs.getBalance()}; } ostream& operator<<(ostream& ostr, const BankAccount& account) { ostr << "Account owner: " << account.getOwner() << "\nAccount balance: " << account.getBalance() << endl; return ostr; } int main() { BankAccount contaDeMarcos {"Marcos", 1000.0}; BankAccount contaConjunta = 1100 + contaDeMarcos; cout << "Saldo da conta de " << contaConjunta.getOwner() << ": " << contaConjunta.getBalance() << endl; return 0; }
A saída do exemplo acima mostra o comportamento que implementei na sobrecarga do operador +: uma concatenação do atributo _owner dos objetos na operação, e a soma dos saldos (_balance) das duas contas bancárias. O resultado é uma espécie de fusão das contas. Para tornar o resultado mais fácil de se ler, adicionei um valor padrão para o atributo _owner da classe BankAccount: Fantasma, e implementei um construtor por parâmetros que recebe apenas o saldo da conta.
Dessa forma, pude criar a variável contaConjunta “somando” 1100 com o objeto contaDeMarcos (linha 53 do exemplo 5). Mas espere um pouco, como raios isso funciona? Isso funciona graças à sobrecarga do operator+ feita como função global (linha 41).
Quando o compilador vê o símbolo + na seguinte expressão: 1100 + contaDeMarcos
, ele encontra a sobrecarga do operador+ na linha 31 e verifica se ela pode ser utilizada. Todavia, essa sobrecarga exige que um objeto da classe BankAccount esteja à esquerda do +, o que não é o caso aqui. O compilador então continua procurando.
Na linha 41, o compilador encontra uma sobrecarga do operador + como função global, cujo primeiro parâmetro é const BankAccount& lhs
, ou seja, uma referência constant para um objeto da classe BankAccount. Contudo, o primeiro argumento no nosso caso é simplesmente o número 1100; como prosseguir? Neste ponto, o compilador procura um construtor da classe BankAccount que aceite apenas um único valor numérico, e ele o encontra!
Na linha 9, o construtor que adicionei nesse exemplo permite construir um objeto do tipo BankAccount simplemente a partir de um número, e é isso que o compilador faz:
- Cria um objeto BankAccount sem nome usando o construtor da linha 9;
- Utiliza a sobrecarga do operador+ como função global (linha 41) para somar o objeto do passo 1 com contaDeMarcos;
- Retorna o resultado da soma e o armazena na variável contaConjunta.
Próximos passos
Chegamos ao final de mais um artigo. Dessa vez aprendemos como fazer sobrecarga de operadores em C++, e vimos que é possível fazê-lo de dois modos: como métodos da classe e como funções globais. Descobrimos quais são as vantagens de se utilizar cada um desses dois tipos, e também quão mais fácil é fazer a sobrecarga dos operadores de comparação no C++20.
Além disso, conhecemos o operador three-way (ou espaçonave) do C++20, que muito nos facilita a vida. Também observamos na prática uma diferença entre o uso de funções globais e métodos de classe na sobrecarga de operadores aritméticos.
Nos artigos que darão sequência a esse assunto, falarei da sobrecarga dos operadores de accesso [] e do operador de chamada de função, os parêntesis (), para que se possa criar objetos que se comportam como funções, os functors. Se não quiser perder os próximos artigos, se inscreva na nossa newsletter usando o campo abaixo. Vejo vocês lá!
Sou apaixonado por tecnologia, literatura e também filosofia. O cultivo dessas paixões ao longo da minha trajetória me inspiraram a compartilhar aquilo que aprendo com os outros da maneira mais clara que eu possa. Sou formado em Engenharia Elétrica no Brasil, e também sou engenheiro formado na França. Trabalho atualmente como programador C++ em uma multinacional francesa, uma das maiores empresas de TI do mundo.