Operadores comuns a sobrecarregar
A maior parte do trabalho em sobrecarregar os operadores é do código da placa da caldeira. Isso não é de admirar, já que os operadores são apenas açúcar sintático, seu trabalho real poderia ser feito por (e muitas vezes é encaminhado para) funções simples. Mas é importante que você consiga esse código de caldeira correto. Se você falhar, o código do seu operador não será compilado ou o código dos seus usuários não será compilado ou o código dos seus usuários se comportará de maneira surpreendente.
Operador de atribuição
Há muito a ser dito sobre a atribuição. No entanto, a maior parte já foi mencionada nas famosas Perguntas frequentes sobre copiar e trocar do GMan , por isso vou pular a maioria aqui, listando apenas o operador de atribuição perfeito para referência:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Operadores Bitshift (usados para E / S de fluxo)
Os operadores de deslocamento de bits <<
e >>
, embora ainda sejam usados na interface de hardware para as funções de manipulação de bits que eles herdam de C, tornaram-se mais prevalentes como operadores de entrada e saída de fluxo sobrecarregados na maioria dos aplicativos. Para obter sobrecarga de orientação como operadores de manipulação de bits, consulte a seção abaixo em Operadores aritméticos binários. Para implementar seu próprio formato customizado e lógica de análise quando seu objeto é usado com iostreams, continue.
Os operadores de fluxo, entre os operadores mais sobrecarregados, são operadores de infixo binário para os quais a sintaxe não especifica nenhuma restrição sobre se devem ser membros ou não membros. Como eles alteram o argumento esquerdo (alteram o estado do fluxo), devem, de acordo com as regras gerais, ser implementados como membros do tipo do operando esquerdo. No entanto, seus operandos esquerdos são fluxos da biblioteca padrão e, enquanto a maioria dos operadores de saída e entrada de fluxo definidos pela biblioteca padrão são realmente definidos como membros das classes de fluxo, quando você implementa operações de saída e entrada para seus próprios tipos, você não pode alterar os tipos de fluxo da biblioteca padrão. É por isso que você precisa implementar esses operadores para seus próprios tipos como funções não membros. As formas canônicas das duas são estas:
std::ostream& operator<<(std::ostream& os, const T& obj)
{
// write obj to stream
return os;
}
std::istream& operator>>(std::istream& is, T& obj)
{
// read obj from stream
if( /* no valid object of T found in stream */ )
is.setstate(std::ios::failbit);
return is;
}
Ao implementar operator>>
, a configuração manual do estado do fluxo só é necessária quando a leitura em si é bem-sucedida, mas o resultado não é o que seria esperado.
Operador de chamada de função
O operador de chamada de função, usado para criar objetos de função, também conhecidos como functores, deve ser definido como uma função membro , para que ele sempre tenha o this
argumento implícito das funções membro. Fora isso, pode ser sobrecarregado para receber qualquer número de argumentos adicionais, incluindo zero.
Aqui está um exemplo da sintaxe:
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
Uso:
foo f;
int a = f("hello");
Em toda a biblioteca padrão C ++, os objetos de função são sempre copiados. Portanto, seus próprios objetos de função devem ser baratos para copiar. Se um objeto de função precisar absolutamente usar dados caros de copiar, é melhor armazenar esses dados em outro local e fazer com que o objeto de função se refira a ele.
Operadores de comparação
Os operadores binários de comparação de infixos devem, de acordo com as regras práticas, ser implementados como funções não membros 1 . A negação de prefixo unário!
deve (de acordo com as mesmas regras) ser implementada como uma função de membro. (mas geralmente não é uma boa idéia sobrecarregá-lo.)
Os algoritmos da biblioteca padrão (por exemplo std::sort()
) e tipos (por exemplo std::map
) sempre esperam operator<
estar presentes. No entanto, os usuários do seu tipo também esperam que todos os outros operadores estejam presentes ; portanto, se você definir operator<
, siga a terceira regra fundamental de sobrecarga de operadores e também defina todos os outros operadores de comparação booleana. A maneira canônica de implementá-los é esta:
inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}
O importante a ser observado aqui é que apenas dois desses operadores realmente fazem alguma coisa, os outros estão apenas encaminhando seus argumentos para esses dois para fazer o trabalho real.
A sintaxe para sobrecarregar os demais operadores booleanos binários ( ||
, &&
) segue as regras dos operadores de comparação. No entanto, é muito improvável que você encontre um caso de uso razoável para esses 2 .
1 Como em todas as regras práticas, às vezes também pode haver razões para quebrar essa. Nesse caso, não esqueça que o operando esquerdo dos operadores de comparação binária, que será para funções-membro *this
, também precisa ser const
. Portanto, um operador de comparação implementado como uma função membro teria que ter esta assinatura:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Observe const
no final.)
2 Note-se que o built-in versão do ||
e &&
semântica uso de atalho. Enquanto os definidos pelo usuário (por serem açúcar sintático para chamadas de método) não usam semântica de atalho. O usuário espera que esses operadores tenham semântica de atalho, e seu código pode depender disso. Portanto, é altamente recomendável NUNCA defini-los.
Operadores aritméticos
Operadores aritméticos unários
Os operadores de incremento e decréscimo unários são fornecidos no formato prefixo e postfix. Para diferenciar uma das outras, as variantes do postfix usam um argumento int adicional adicional. Se você sobrecarregar o incremento ou o decremento, sempre implemente as versões de prefixo e postfix. Aqui está a implementação canônica do incremento, o decremento segue as mesmas regras:
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
Observe que a variante postfix é implementada em termos de prefixo. Observe também que o postfix faz uma cópia extra. 2
Sobrecarregar menos e mais unário não é muito comum e provavelmente é melhor evitar. Se necessário, eles provavelmente devem estar sobrecarregados como funções de membro.
2 Observe também que a variante postfix funciona mais e, portanto, é menos eficiente do que a variante prefixo. Esse é um bom motivo para preferir geralmente o incremento do prefixo ao incremento do pós-fixado. Embora os compiladores geralmente possam otimizar o trabalho adicional de incremento do postfix para tipos internos, eles podem não ser capazes de fazer o mesmo para tipos definidos pelo usuário (que podem parecer tão inocentemente quanto um iterador de lista). Depois que você se acostuma i++
, é muito difícil lembrar de fazer ++i
quando i
não é do tipo interno (além disso, você precisa alterar o código ao alterar um tipo); portanto, é melhor criar o hábito de sempre usando incremento de prefixo, a menos que o postfix seja explicitamente necessário.
Operadores aritméticos binários
Para os operadores aritméticos binários, não se esqueça de obedecer à terceira sobrecarga de operadores de regras básicas: se você fornecer +
, também fornecer +=
, se fornecer -
, não omitir -=
etc. etc. Diz-se que Andrew Koenig foi o primeiro a observar que a atribuição composta operadores podem ser usados como base para suas contrapartes não compostas. Ou seja, o operador +
é implementado em termos de +=
, -
é implementado em termos de -=
etc.
De acordo com nossas regras práticas, +
e seus companheiros devem ser não membros, enquanto seus colegas de designação composta ( +=
etc.), alterando seu argumento à esquerda, devem ser membros. Aqui está o código exemplar para +=
e +
; os outros operadores aritméticos binários devem ser implementados da mesma maneira:
class X {
X& operator+=(const X& rhs)
{
// actual addition of rhs to *this
return *this;
}
};
inline X operator+(X lhs, const X& rhs)
{
lhs += rhs;
return lhs;
}
operator+=
retorna o resultado por referência, enquanto operator+
retorna uma cópia do resultado. Obviamente, retornar uma referência geralmente é mais eficiente do que retornar uma cópia, mas, no caso de operator+
, não há como contornar a cópia. Quando você escreve a + b
, espera que o resultado seja um novo valor, e é por isso operator+
que deve retornar um novo valor. 3
Observe também que operator+
pega o operando esquerdo por cópia, e não por referência const. A razão para isso é a mesma que a razão paraoperator=
o argumento por cópia.
Os operadores de manipulação de bits ~
&
|
^
<<
>>
devem ser implementados da mesma maneira que os operadores aritméticos. No entanto, (exceto para sobrecarga <<
e>>
saída e entrada), existem muito poucos casos de uso razoáveis para sobrecarregá-los.
3 Novamente, a lição a ser tirada disso é que a += b
, em geral, é mais eficiente a + b
e deve ser preferível, se possível.
Subscrição de Matrizes
O operador de subscrito da matriz é um operador binário que deve ser implementado como um membro da classe. É usado para tipos semelhantes a contêineres que permitem acesso aos seus elementos de dados por uma chave. A forma canônica de fornecê-los é a seguinte:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
A menos que você não queira que os usuários da sua classe possam alterar os elementos de dados retornados por operator[]
(nesse caso, você pode omitir a variante não-const), sempre forneça as duas variantes do operador.
Se value_type é conhecido por se referir a um tipo interno, a variante const do operador deve retornar melhor uma cópia em vez de uma referência const:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Operadores para tipos semelhantes a ponteiros
Para definir seus próprios iteradores ou ponteiros inteligentes, é necessário sobrecarregar o operador de desreferência de prefixo unário *
e o operador de acesso ao membro do ponteiro de infixo binário ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
Observe que eles também quase sempre precisam de uma versão const e uma não-const. Para o ->
operador, se value_type
for do tipo class
(ou struct
ou union
), outro operator->()
é chamado recursivamente, até que um operator->()
retorne um valor do tipo não pertencente à classe.
O endereço unário do operador nunca deve ser sobrecarregado.
Para operator->*()
ver esta pergunta . É raramente usado e, portanto, raramente sobrecarregado. De fato, mesmo os iteradores não sobrecarregam.
Continue para Operadores de conversão