Quais são as regras e expressões básicas para sobrecarga do operador?

2144

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

(Observação: isso deve ser uma entrada para as Perguntas frequentes sobre C ++ do Stack Overflow . Se você quiser criticar a idéia de fornecer uma FAQ neste formulário, a postagem na meta que iniciou tudo isso seria o lugar para isso. essa pergunta é monitorada na sala de chat do C ++ , onde a idéia de FAQ começou em primeiro lugar; portanto, é muito provável que sua resposta seja lida pelos que a tiveram.)

sbi
fonte
63
Se continuarmos com a tag C ++ - FAQ, é assim que as entradas devem ser formatadas.
John Dibling
Escrevi uma curta série de artigos para a comunidade C ++ alemã sobre sobrecarga de operadores: Parte 1: a sobrecarga de operadores em C ++ abrange semântica, uso típico e especialidades para todos os operadores. Há algumas sobreposições com suas respostas aqui, no entanto, há algumas informações adicionais. As partes 2 e 3 fazem um tutorial para usar o Boost.Operators. Deseja que eu os traduza e os adicione como respostas?
Arne Mertz
Ah, e uma tradução em Inglês também está disponível: o básico e prática comum
Arne Mertz

Respostas:

1044

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 thisargumento 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 constno 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 ++iquando inã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 + be 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_typefor do tipo class(ou structou 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

sbi
fonte
89
operator->()é realmente extremamente estranho. Não é necessário retornar um value_type*- na verdade, ele pode retornar outro tipo de classe, desde que o tipo de classe possua umoperator->() , que será chamado posteriormente. Essa chamada recursiva de operator->()s continua até que um value_type*tipo de retorno ocorra. Loucura! :)
j_random_hacker
2
Não é exatamente sobre eficácia. É sobre não podermos fazê-lo da maneira idiomática tradicional em (muito) poucos casos: quando a definição de ambos os operandos precisa permanecer inalterada enquanto calculamos o resultado. E como eu disse, existem dois exemplos clássicos: multiplicação de matrizes e multiplicação de polinômios. Poderíamos definir *em termos de, *=mas seria estranho, porque uma das primeiras operações de *=criaria um novo objeto, resultado da computação. Depois, após o loop for-ijk, trocaríamos esse objeto temporário por *this. ie 1.copy, 2.operator *, 3.swap
Luc Hermitte
6
Eu discordo das versões const / non-const dos seus operadores do tipo ponteiro, por exemplo, `const value_type & operator * () const;` - isso seria como ter um T* constretorno de uma const T&dreferenciação, o que não é o caso. Ou, em outras palavras: um ponteiro const não implica um ponteiro const. De fato, não é trivial imitar T const *- que é a razão de todo o const_iteratormaterial na biblioteca padrão. Conclusão: a assinatura deve serreference_type operator*() const; pointer_type operator->() const
Arne Mertz
6
Um comentário: A implementação de operadores aritméticos binários sugerida não é tão eficiente quanto pode ser. SE operadores impulso cabeçalhos de simetria nota: boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry Mais uma cópia pode ser evitado se você usar uma cópia local do primeiro parâmetro, não + =, e devolver o cópia local. Isso permite a otimização do NRVO.
Manu343726
3
Como mencionei no chat, L <= Rtambém pode ser expresso como em !(R < L)vez de !(L > R). Pode salvar uma camada extra de embutimento em expressões difíceis de otimizar (e também é como o Boost.Operators o implementa).
TemplateRex
494

As três regras básicas de sobrecarga de operadores 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 de fato exceções. Às vezes, as pessoas se desviaram delas e o resultado não foi um código ruim, mas esses desvios positivos são poucos e distantes entre si. No mínimo, 99 dos 100 desvios que eu vi foram injustificados. No entanto, pode ter sido 999 em 1000. Portanto, é melhor seguir as regras a seguir.

  1. Sempre que o significado de um operador não for obviamente claro e indiscutível, ele não deverá ser sobrecarregado. Em vez disso, forneça uma função com um nome bem escolhido.
    Basicamente, a primeira e principal regra para sobrecarregar os operadores, no fundo, diz: Não faça isso . Isso pode parecer estranho, porque há muito a ser conhecido sobre a sobrecarga do operador e, portanto, muitos artigos, capítulos de livros e outros textos lidam com tudo isso. Mas, apesar dessa evidência aparentemente óbvia, há apenas surpreendentemente poucos casos em que a sobrecarga do operador é apropriada. O motivo é 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, esse quase nunca é o caso.

  2. Sempre mantenha a 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+operadorbináriopara subtrair do operando direito. No entanto, os utilizadores de um operador como nunca suspeitaria a expressãoa + bpara subtrairaa partirb. Obviamente, isso supõe que a semântica do operador no domínio do aplicativo seja indiscutível.

  3. Sempre forneça tudo de um conjunto de operações relacionadas.
    Os operadores estão relacionados entre si e com outras operações. Se o seu tipo suportara + b, os usuários esperam poder ligara += btambém. Se ele suportar incremento de prefixo++a, eles esperama++que funcione também. Se eles puderem verificar sea < b, certamente esperarão também poder verificar sea > b. Se eles podem copiar e construir seu tipo, esperam que a atribuição funcione também.


Continue em A decisão entre membro e não membro .

sbi
fonte
16
A única coisa que sei que viola qualquer uma dessas coisas é boost::spiritlol.
quer
66
@ Billy: De acordo com alguns, abusar +da concatenação de cordas é uma violação, mas agora se tornou uma práxis bem estabelecida, de modo que parece natural. Embora eu me lembre de uma classe de cordas caseira que vi nos anos 90 que usava o binário &para esse fim (referindo-se ao BASIC para a prática estabelecida). Mas, sim, colocá-lo na lib std basicamente define isso em pedra. O mesmo vale para abusar <<e >>para IO, BTW. Por que a mudança à esquerda seria a operação de saída óbvia? Porque todos nós aprendemos sobre isso quando vimos nosso primeiro "Olá, mundo!" inscrição. E por nenhuma outra razão.
S12/
5
@curiousguy: Se você tem que explicar, não é obviamente claro e indiscutível. Da mesma forma, se você precisar discutir ou defender a sobrecarga.
S2
5
@sbi: "revisão por pares" é sempre uma boa ideia. Para mim, um operador mal escolhido não é diferente de um nome de função mal escolhido (eu vi muitos). Operador são apenas funções. Nem mais nem menos. As regras são iguais. E para entender se uma ideia é boa, a melhor maneira é entender quanto tempo leva para ser entendido. (Daí, revisão por pares é uma obrigação, mas os pares devem ser escolhidos entre pessoas livres de dogmas e preconceitos.)
Emilio Garavaglia
5
@sbi Para mim, o único fato absolutamente óbvio e incontestável operator==é que deve haver uma relação de equivalência (IOW, você não deve usar NaN sem sinalização). Existem muitas relações úteis de equivalência em contêineres. O que significa igualdade? " aigual b" significa isso ae btem o mesmo valor matemático. O conceito de valor matemático de a (não NaN) floaté claro, mas o valor matemático de um contêiner pode ter muitas definições úteis distintas (tipo recursiva). A definição mais forte de igualdade é "eles são os mesmos objetos" e é inútil.
curiousguy
265

A sintaxe geral da sobrecarga de operadores em C ++

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

Nem todos os operadores podem estar 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 ++ estão:

  • operadores aritméticos: + - * / %e += -= *= /= %=(todo o infixo binário); + -(prefixo unário); ++ --(prefixo unário e postfix)
  • manipulação de bits: & | ^ << >>e &= |= ^= <<= >>=(todo o 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ícitos
  • miscelânea: = [] -> ->* , (todo o infixo binário); * &(prefixo todo unário) ()(chamada de função, infixo n-ário)

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

No C ++, os operadores são sobrecarregados na forma de funções com nomes especiais . Como em outras funções, operadores sobrecarregados geralmente podem ser implementados como uma função membro do tipo do operando esquerdo ou como funções não membros . 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 operator@(x)ou como x.operator@(). Um operador de infixo binário @, aplicado aos objetos xe y, é chamado como operator@(x,y)ou como x.operator@(y). 4

Às vezes, os operadores implementados como funções não membros são amigos do tipo de seu operando.

1 O termo "definido pelo usuário" pode ser um pouco enganador. O C ++ faz a distinção entre tipos internos e tipos definidos pelo usuário. Para os primeiros pertencem, por exemplo, int, char e double; a este último pertence todos os tipos de struct, classe, união e enum, incluindo os da biblioteca padrão, mesmo que não sejam, como tal, definidos pelos usuários.

2 Isso será abordado em uma parte posterior desta FAQ.

3 O @operador não é 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 com as três regras básicas de sobrecarga de operadores em C ++ .

sbi
fonte
~é prefixo unário, não infixo binário.
Mrkj 2/11
1
.*está ausente na lista de operadores não sobrecarregáveis.
Celticminstrel
1
@ Mateen Eu queria usar um espaço reservado em vez de um operador real, a fim de deixar claro que não se trata de um operador especial, mas se aplica a todos eles. E, se você quer ser um programador de C ++, deve aprender a prestar atenção, mesmo em letras pequenas. :)
Sbi 11/11
1
@ HR: Se você leu este guia, saberia o que está errado. Em geral, sugiro que você leia as três primeiras respostas vinculadas à pergunta. Isso não deve durar mais de meia hora da sua vida e fornece um entendimento básico. A sintaxe específica do operador, você pode procurar mais tarde. Seu problema específico sugere que você tente sobrecarregar operator+()como uma função membro, mas deu a assinatura de uma função livre. Veja aqui .
SBI
1
@bi: Eu já li os três primeiros posts e obrigado por fazê-los. :) Vou tentar resolver o problema, caso contrário, acho que é melhor fazer isso em uma pergunta separada. Obrigado novamente por tornar a vida tão fácil para nós! : D
Hosein Rahnama
251

A decisão entre membro e não membro

Os operadores binários =(atribuição), [](assinatura da matriz), ->(acesso de membro), bem como o ()operador n-ária (chamada de função), sempre devem ser implementados como funções de membro , porque a sintaxe do idioma exige isso.

Outros operadores podem ser implementados como membros ou como não membros. Alguns deles, no entanto, geralmente precisam ser implementados como funções que não são membros, porque o operando esquerdo não pode ser modificado por você. Os mais destacados são os operadores de entrada e saída <<e >>, cujos operandos esquerdos são classes de fluxo da biblioteca padrão que você não pode alterar.

Para todos os operadores nos quais você deve optar por implementá-los como uma função membro ou não, use as seguintes regras práticas para decidir:

  1. Se for um operador unário , implemente-o como uma função de membro .
  2. Se um operador binário tratar os dois operandos igualmente (os deixa inalterados), implemente esse operador como uma função não membro .
  3. Se um operador binário não tratar os dois operandos da mesma forma (normalmente ele mudará o operando esquerdo), pode ser útil torná-lo uma função membro do tipo do operando esquerdo, se precisar acessar as partes privadas do operando.

Obviamente, como em todas as regras de ouro, há exceções. Se você tem um tipo

enum Month {Jan, Feb, ..., Nov, Dec}

e você deseja sobrecarregar os operadores de incremento e decremento, não é possível fazer isso como uma função membro, pois em C ++, os tipos de enumeração não podem ter funções membro. Então você precisa sobrecarregá-lo como uma função livre. E operator<()para um modelo de classe aninhado dentro de um modelo de classe é muito mais fácil escrever e ler quando executado como uma função de membro embutida na definição de classe. Mas essas são de fato raras exceções.

(No entanto, se você fizer uma exceção, não se esqueça da questão de const-ness para o operando que, para funções-membro, se torna o thisargumento implícito . Se o operador como uma função não-membro usaria seu argumento mais à esquerda como constreferência , o mesmo operador que uma função membro precisa ter um constno final para fazer*this uma constreferência.)


Continue com os operadores comuns para sobrecarregar .

sbi
fonte
9
O item de Herb Sutter em Effective C ++ (ou é C ++ Coding Standards?) Diz que se deve preferir funções de não-membro não-amigo a funções de membro, para aumentar o encapsulamento da classe. IMHO, o motivo do encapsulamento tem precedência para sua regra de ouro, mas não diminui o valor da qualidade de sua regra de ouro.
paercebal
8
@ paercebal: C ++ eficaz é de Meyers, C ++ Coding Standards de Sutter. A qual você está se referindo? De qualquer forma, não gosto da idéia de, por exemplo, operator+=()não ser um membro. Ele precisa mudar seu operando do lado esquerdo, portanto, por definição, precisa se aprofundar em suas entranhas. O que você ganharia se não fosse um membro?
S12
9
@sbi: Item 44 em C ++ Coding Standards (Sutter) Prefere escrever funções de não-membros que não sejam membros , é claro, isso só se aplica se você puder realmente escrever essa função usando apenas a interface pública da classe. Se você não pode (ou pode, mas isso prejudicaria muito o desempenho), é necessário torná-lo membro ou amigo.
Matthieu M.
3
@sbi: Opa, eficaz, excepcional ... Não é de admirar que eu misture os nomes. De qualquer forma, o ganho é limitar o máximo possível o número de funções que têm acesso a um objeto de dados privados / protegidos. Dessa forma, você aumenta o encapsulamento de sua classe, facilitando sua manutenção / teste / evolução.
paercebal
12
@bi: Um exemplo. Digamos que você esteja codificando uma classe String, com operator +=os appendmétodos e. O appendmétodo é mais completo, porque você pode anexar uma substring do parâmetro do índice i ao índice n -1: append(string, start, end)parece lógico ter +=o acréscimo de chamada com start = 0e end = string.size. Nesse momento, o acréscimo poderia ser um método de membro, mas operator +=não precisa ser um membro, e torná-lo um não-membro diminuiria a quantidade de código reproduzida com as entranhas String, por isso é uma coisa boa ... ^ _ ^ ...
paercebal
165

Operadores de conversão (também conhecidos como conversões definidas pelo usuário)

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

Operadores implícitos de conversão (C ++ 98 / C ++ 03 e C ++ 11)

Um operador de conversão implícita permite que o compilador converta implicitamente (como a conversão entre inte long) o valor de um tipo definido pelo usuário para outro tipo.

A seguir, é apresentada 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ícitos, 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 entra em ação quando não se espera. 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());

Iniciantes facilmente entendem isso errado e até programadores C ++ experientes às vezes são surpreendidos porque o compilador pega uma sobrecarga que não suspeitava. Esses problemas podem ser atenuados por operadores de conversão explícitos.

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

Ao contrário dos operadores de conversão implícitos, os operadores de conversão explícitos nunca entram em ação 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ícitos, obtém um erro do compilador:

prog.cpp: Na função 'int main ()':
prog.cpp: 15: 18: error: nenhuma função correspondente à chamada para 'f (minha_string)'
prog.cpp: 15: 18: note: os candidatos são:
prog.cpp: 11: 10: note: void f (minha_string &)
prog.cpp: 11: 10: note: nenhuma conversão conhecida para o argumento 1 de 'my_string' para 'my_string &'
prog.cpp: 12: 10: note: void f (const char *)
prog.cpp: 12: 10: note: nenhuma conversão conhecida para o argumento 1 de 'my_string' para 'const char *'

Para chamar o operador de conversão explícita, você deve usar static_castuma conversão no estilo C ou uma conversão no estilo construtor (ou seja T(value)).

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

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

Continue com Sobrecarga newedelete .

JKor
fonte
148

Sobrecarga newedelete

Nota: Isso lida apenas com a sintaxe de sobrecarganewedelete, não com a implementação desses operadores sobrecarregados. Eu acho que a semântica da sobrecarganew e deletemerece sua própria FAQ , dentro do tópico sobrecarga do operador, nunca posso fazer justiça.

Fundamentos

Em C ++, quando você escrever uma nova expressão como new T(arg)duas coisas acontecem quando esta expressão é avaliada: Primeiro operator newé invocado para obter a memória bruta, e, em seguida, o construtor adequada de Té invocado para transformar esta memória bruto em um objeto válido. Da mesma forma, quando você exclui um objeto, primeiro seu destruidor é chamado e, em seguida, a memória é retornada operator delete.
O C ++ permite ajustar essas duas 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 gerenciamento de ajuste de memória é feito escrevendo seu próprio operator newe operator delete.

A primeira das regras básicas da sobrecarga do operador - não faça isso - se aplica especialmente à sobrecarga newe delete. Quase os únicos motivos 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 utilizados, 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 newe predefinidos delete. 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 sobrecarregarão, mas substituirão as da biblioteca padrão.
Se você sobrecarregar operator new, sempre sobrecarregue a correspondência operator delete, mesmo que nunca pretenda chamá-la. O motivo é que, se um construtor lança durante a avaliação de uma nova expressão, o sistema de tempo de execução retornará a memória para a operator deletecorrespondência operator newque foi chamada para alocar a memória para a criação do objeto. Se você não fornecer uma correspondência operator delete, o padrão é chamado, o que quase sempre está errado.
Se você sobrecarregar newe delete, também deverá sobrecarregar as variantes da matriz.

Canal new

O C ++ permite que operadores novos e excluídos recebam argumentos adicionais.
O chamado posicionamento new permite criar 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 fornecido acima, operator deletenunca é chamado, a menos que o construtor de X gere uma exceção.

Você também pode sobrecarregar newe deletecom outros argumentos. Como no 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, essas variantes também são chamadas de posicionamento novo, mesmo que seus argumentos não sejam para colocar um objeto em um endereço específico.

Novo e excluir específico da classe

Geralmente, você deseja ajustar o gerenciamento de memória porque a medição mostrou que as instâncias de uma classe específica ou de um grupo de classes relacionadas são criadas e destruídas frequentemente e que o gerenciamento de memória padrão do sistema de tempo de execução, ajustado para desempenho geral, lida de forma ineficiente nesse caso específico. Para melhorar isso, você pode sobrecarregar new e delete 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áticas. Para objetos de my_class, o std::size_targumento sempre será sizeof(my_class). No entanto, esses operadores também são chamados para objetos alocados dinamicamente de classes derivadas ; nesse caso, pode ser maior que isso.

Novo global e exclusão

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

sbi
fonte
11
Também não concordo que substituir o operador global new e delete seja geralmente para desempenho: pelo contrário, geralmente é para rastreamento de erros.
Yttrill
1
Você também deve observar que, se você usar um novo operador sobrecarregado, precisará fornecer também um operador de exclusão com argumentos correspondentes. Você diz isso na seção global new / delete onde não é de grande interesse.
Yttrill
13
@ Ytrtrill você está confundindo as coisas. O significado fica sobrecarregado. O que significa "sobrecarga do operador" significa que o significado está sobrecarregado. Isso não significa que literalmente as funções estejam sobrecarregadas e, em particular, o operador new não sobrecarregará a versão do Standard. @sbi não afirma o contrário. É comum chamá-lo de "sobrecarregar novo" da mesma forma que é comum dizer "sobrecarregar operador de adição".
Johannes Schaub - litb
1
@sbi: Veja (ou melhor, link para) gotw.ca/publications/mill15.htm . É apenas uma boa prática para pessoas que às vezes usam nothrownovas.
Alexandre C.
1
"Se você não fornecer uma exclusão de operador correspondente, a padrão será chamada" -> Na verdade, se você adicionar algum argumento e não criar uma exclusão correspondente, nenhuma exclusão de operador será chamada e ocorrerá um vazamento de memória. (15.2.2, o espaço ocupado pelo objeto é desalocada somente se um apropriado ... operador de exclusão é encontrado)
dascandy
46

Por que a operator<<função de transmitir objetos para std::coutou para um arquivo não pode ser uma função membro?

Digamos que você tenha:

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 membro de Foo, o LHS do operador deve ser um Fooobjeto. O que significa que você precisará usar:

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

o que é muito não 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.

R Sahu
fonte