c++ - funções - Quais são as regras básicas e expressões idiomáticas para sobrecarga de operadores?




sobrecarga operador== c++ (5)

A sintaxe geral de sobrecarga de operador em C ++

Você não pode alterar o significado de operadores para tipos internos em C ++, os operadores só podem ser sobrecarregados para tipos definidos pelo usuário 1 . Ou seja, pelo menos um dos operandos deve ser de um tipo definido pelo usuário. Tal como acontece com outras funções sobrecarregadas, os operadores podem ser sobrecarregados para um determinado conjunto de parâmetros apenas uma vez.

Nem todos os operadores podem ser sobrecarregados em C ++. Entre os operadores que não podem ser sobrecarregados estão:. :: sizeof typeid .* e o único operador ternário em C ++, ?:

Entre os operadores que podem ser sobrecarregados em C ++ são estes:

  • operadores aritméticos: + - * / % e += -= *= /= %= (todo infixo binário); + - (prefixo unário); ++ -- (prefixo unário e postfix)
  • manipulação de bits: & | ^ << >> e &= |= ^= <<= >>= (todo infixo binário); ~ (prefixo unário)
  • álgebra booleana: == != < > || >= || && (todo o infixo binário); ! (prefixo unário)
  • gerenciamento de memória: new new[] delete delete[]
  • operadores de conversão implícita
  • miscelânea: = [] -> ->* , (todo infixo binário); * & (todo prefixo unário) () (chamada de função, infixo n-ário)

No entanto, o fato de você poder sobrecarregar tudo isso não significa que você deva fazê-lo. Veja as regras básicas de sobrecarga do operador.

Em C ++, os operadores são sobrecarregados na forma de funções com nomes especiais . Como com outras funções, os operadores sobrecarregados geralmente podem ser implementados como uma função de membro do tipo de seu operando esquerdo ou como funções de não membro . Se você é livre para escolher ou obrigado a usar qualquer um deles depende de vários critérios. 2 Um operador unário @ 3 , aplicado a um objeto x, é chamado como [email protected](x) ou como [email protected]() . Um operador binário infixo @ , aplicado aos objetos x e y , é chamado de [email protected](x,y) ou de [email protected](y) . 4

Os operadores que são implementados como funções não-membros são às vezes amigos do tipo de seus operandos.

1 O termo “definido pelo usuário” pode ser um pouco enganador. C ++ faz a distinção entre tipos internos e tipos definidos pelo usuário. Para o primeiro, por exemplo, int, char e double; para o último pertencem todos os tipos de struct, class, union e enum, incluindo aqueles da biblioteca padrão, mesmo que eles não sejam, como tal, definidos pelos usuários.

2 Isso é abordado em uma parte posterior desta FAQ.

3 O @ não é um operador válido em C ++, e é por isso que o uso como espaço reservado.

4 O único operador ternário em C ++ não pode ser sobrecarregado e o único operador n-ário deve sempre ser implementado como uma função-membro.

Continue para as três regras básicas de sobrecarga do operador em C ++ .

Nota: As respostas foram dadas em uma ordem específica , mas como muitos usuários classificam as respostas de acordo com os votos, em vez do tempo que receberam, aqui está um índice das respostas na ordem em que fazem mais sentido:

(Nota: Esta é uma entrada para o C ++ FAQ do Stack Overflow . Se você quiser criticar a idéia de fornecer um FAQ neste formulário, então o post no meta que iniciou tudo isso seria o lugar para fazer isso. essa questão é monitorada na sala de chat do C ++ , onde a ideia do FAQ começou em primeiro lugar, então é muito provável que sua resposta seja lida por aqueles que surgiram com a ideia.)


As três regras básicas de sobrecarga do operador em C ++

Quando se trata de sobrecarga de operadores em C ++, existem três regras básicas que você deve seguir . Como com todas essas regras, existem exceções. Às vezes as pessoas se desviaram delas e o resultado não foi um código ruim, mas tais desvios positivos são poucos e distantes entre si. No mínimo, 99 de 100 desses desvios que vi foram injustificados. No entanto, poderia muito bem ter sido 999 de 1000. Então é melhor você seguir as seguintes regras.

  1. Sempre que o significado de um operador não é obviamente claro e indiscutível, ele não deve ser sobrecarregado. Em vez disso, forneça uma função com um nome bem escolhido.
    Basicamente, a primeira e mais importante regra para sobrecarregar os operadores, em seu coração, diz: não faça isso . Isso pode parecer estranho, porque há muito a ser conhecido sobre a sobrecarga de operadores e, portanto, muitos artigos, capítulos de livros e outros textos lidam com tudo isso. Mas, apesar dessas evidências aparentemente óbvias, há apenas alguns casos surpreendentes em que a sobrecarga de operadores é apropriada . A razão é que, na verdade, é difícil entender a semântica por trás da aplicação de um operador, a menos que o uso do operador no domínio do aplicativo seja bem conhecido e indiscutível. Ao contrário da crença popular, isso dificilmente é o caso.

  2. Sempre se atenha à semântica conhecida do operador.
    C ++ não apresenta limitações na semântica de operadores sobrecarregados. Seu compilador aceitará com prazer o código que implementa o operador binário + para subtrair de seu operando direito. No entanto, os usuários de tal operador nunca suspeitariam que a expressão a + b subtraia a de b . Naturalmente, isso supõe que a semântica do operador no domínio do aplicativo é indiscutível.

  3. Sempre forneça tudo de um conjunto de operações relacionadas.
    Os operadores estão relacionados uns com os outros e com outras operações. Se o seu tipo suportar a + b , os usuários poderão chamar a += b também. Se ele suportar incremento de prefixo ++a , eles esperarão que a++ funcione também. Se eles puderem verificar se a < b , eles certamente esperarão também ser capazes de verificar se a > b . Se eles podem copiar o seu tipo, eles esperam que a atribuição funcione também.

Continue a decisão entre membro e não membro .


Operadores de Conversão (também conhecidos como Conversões Definidas pelo Usuário)

Em C ++ você pode criar operadores de conversão, operadores que permitem ao compilador converter entre seus tipos e outros tipos definidos. Existem dois tipos de operadores de conversão, os implícitos e explícitos.

Operadores de Conversão Implícita (C ++ 98 / C ++ 03 e C ++ 11)

Um operador de conversão implícito permite que o compilador converta implicitamente (como a conversão entre int e long ) o valor de um tipo definido pelo usuário em algum outro tipo.

A seguir, uma classe simples com um operador de conversão implícito:

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

Operadores de conversão implícita, como construtores de um argumento, são conversões definidas pelo usuário. Os compiladores concederão uma conversão definida pelo usuário ao tentar corresponder uma chamada a uma função sobrecarregada.

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

No começo, isso parece muito útil, mas o problema é que a conversão implícita até entra em ação quando não é esperado. No código a seguir, void f(const char*) será chamado porque my_string() não é um lvalue , portanto, o primeiro não corresponde:

void f(my_string&);
void f(const char*);

f(my_string());

Os iniciantes facilmente percebem isso errado e até mesmo os programadores experientes de C ++ ficam surpresos porque o compilador escolhe uma sobrecarga que eles não suspeitavam. Esses problemas podem ser atenuados por operadores de conversão explícitos.

Operadores explícitos de conversão (C ++ 11)

Ao contrário dos operadores de conversão implícita, os operadores de conversão explícita nunca serão acionados quando você não espera. A seguir, uma classe simples com um operador de conversão explícito:

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

Observe o explicit . Agora, quando você tenta executar o código inesperado dos operadores de conversão implícita, você obtém um erro do compilador:

prog.cpp: In function ‘int main()’:
prog.cpp:15:18: error: no matching function for call to ‘f(my_string)’
prog.cpp:15:18: note: candidates are:
prog.cpp:11:10: note: void f(my_string&)
prog.cpp:11:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘my_string&’
prog.cpp:12:10: note: void f(const char*)
prog.cpp:12:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘const char*’

Para invocar o operador de static_cast explícito, você tem que usar static_cast , uma static_cast de estilo C ou uma conversão de estilo de construtor (ou seja, T(value) ).

No entanto, há uma exceção para isso: O compilador tem permissão para converter implicitamente em bool . Além disso, o compilador não pode fazer outra conversão implícita depois de convertê-lo em bool (um compilador pode fazer 2 conversões implícitas de cada vez, mas apenas 1 conversão definida pelo usuário no máximo).

Como o compilador não converterá o bool "passado", os operadores de conversão explícita agora removerão a necessidade do idioma do Bool Seguro . Por exemplo, os ponteiros inteligentes antes do C ++ 11 usavam o idioma Safe Bool para evitar conversões para tipos integrais. Em C ++ 11, os ponteiros inteligentes usam um operador explícito em vez disso, porque o compilador não tem permissão para converter implicitamente em um tipo integral após converter explicitamente um tipo em bool.

Continue a Sobrecarga e delete .


Sobrecarga de new e delete

Nota: Isso lida apenas com a sintaxe de sobrecarregar new e delete , não com a implementação de tais operadores sobrecarregados. Eu acho que a semântica de sobrecarregar new e delete merece seu próprio FAQ , dentro do tópico de sobrecarga de operador eu nunca posso fazer justiça a ele.

Noções básicas

Em C ++, quando você escreve uma nova expressão como new T(arg) duas coisas acontecem quando esta expressão é avaliada: Primeiro operator new é invocado para obter memória bruta, e então o construtor apropriado de T é invocado para transformar essa memória bruta em um objeto válido. Da mesma forma, quando você exclui um objeto, primeiro seu destruidor é chamado e, em seguida, a memória é retornada ao operator delete .
C ++ permite que você ajuste ambas as operações: gerenciamento de memória e construção / destruição do objeto na memória alocada. O último é feito escrevendo construtores e destruidores para uma classe. O ajuste fino do gerenciamento de memória é feito escrevendo seu próprio operator new e operator delete .

A primeira das regras básicas de sobrecarga de operadores - não faça isso - aplica-se especialmente à sobrecarga de new e delete . Quase as únicas razões para sobrecarregar esses operadores são problemas de desempenho e restrições de memória e, em muitos casos, outras ações, como alterações nos algoritmos usados, fornecerão uma relação custo / ganho muito maior do que tentar ajustar o gerenciamento de memória.

A biblioteca padrão C ++ vem com um conjunto de operadores new e delete predefinidos. Os mais importantes são estes:

void* operator new(std::size_t) throw(std::bad_alloc); 
void  operator delete(void*) throw(); 
void* operator new[](std::size_t) throw(std::bad_alloc); 
void  operator delete[](void*) throw(); 

Os dois primeiros alocam / desalocam memória para um objeto, os dois últimos para uma matriz de objetos. Se você fornecer suas próprias versões, elas não serão sobrecarregadas, mas substituirão as da biblioteca padrão.
Se você sobrecarregar o operator new , você deve sempre sobrecarregar o operator delete correspondente operator delete , mesmo se você nunca pretende chamá-lo. A razão é que, se um construtor lançar durante a avaliação de uma nova expressão, o sistema de tempo de execução retornará a memória ao operator delete correspondente ao operator new que foi chamado para alocar a memória na qual criar o objeto. não fornece uma operator delete correspondente, a chamada padrão é chamada, o que é quase sempre errado.
Se você sobrecarregar new e delete , você deve considerar sobrecarregar as variantes do array também.

Posicionamento new

C ++ permite que operadores novos e excluídos utilizem argumentos adicionais.
O novo posicionamento permite que você crie um objeto em um determinado endereço que é passado para:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

A biblioteca padrão vem com as sobrecargas apropriadas dos operadores new e delete para isso:

void* operator new(std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete(void* p,void*) throw(); 
void* operator new[](std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete[](void* p,void*) throw(); 

Observe que, no código de exemplo para posicionamento novo dado acima, operator delete nunca é chamado, a menos que o construtor de X lance uma exceção.

Você também pode sobrecarregar new e delete com outros argumentos.Como com o argumento adicional para posicionamento novo, esses argumentos também são listados entre parênteses após a palavra-chave new. Apenas por razões históricas, tais variantes são freqüentemente chamadas de nova colocação, mesmo que seus argumentos não sejam para colocar um objeto em um endereço específico.

Novo e específico da classe

Mais comumente você desejará ajustar o gerenciamento de memória porque a medição mostrou que instâncias de uma classe específica, ou de um grupo de classes relacionadas, são criadas e destruídas com freqüência e que o gerenciamento de memória padrão do sistema de tempo de execução, sintonizado desempenho geral, lida de forma ineficiente neste caso específico. Para melhorar isso, você pode sobrecarregar novos e excluir para uma classe específica:

class my_class { 
  public: 
    // ... 
    void* operator new();
    void  operator delete(void*,std::size_t);
    void* operator new[](size_t);
    void  operator delete[](void*,std::size_t);
    // ... 
}; 

Sobrecarregado assim, new e delete se comportam como funções de membro estático. Para objetos de my_class, o std::size_targumento será sempre sizeof(my_class). No entanto, esses operadores também são chamados de objetos alocados dinamicamente de classes derivadas , caso em que pode ser maior que isso.

Global novo e excluir

Para sobrecarregar o global new e delete, basta substituir os operadores predefinidos da biblioteca padrão pelos nossos. No entanto, isso raramente precisa ser feito.


Por que não operator<<funciona para streaming de objetos para std::coutou para um arquivo ser uma função-membro?

Vamos dizer que você tem:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

Dado isso, você não pode usar:

Foo f = {10, 20.0};
std::cout << f;

Como operator<<está sobrecarregado como uma função de membro Foo, o LHS do operador deve ser um Fooobjeto. O que significa que você será obrigado a usar:

Foo f = {10, 20.0};
f << std::cout

o que é muito pouco intuitivo.

Se você defini-lo como uma função não membro,

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

Você poderá usar:

Foo f = {10, 20.0};
std::cout << f;

o que é muito intuitivo.





c++-faq