Copiar construtor e = sobrecarga de operador em C ++: uma função comum é possível?

87

Desde um construtor de cópia

MyClass(const MyClass&);

e uma = sobrecarga de operador

MyClass& operator = (const MyClass&);

tem praticamente o mesmo código, o mesmo parâmetro, e só diferem no retorno, é possível ter uma função comum para os dois usarem?

MPelletier
fonte
6
"... tem praticamente o mesmo código ..."? Hmm ... Você deve estar fazendo algo errado. Tente minimizar a necessidade de usar funções definidas pelo usuário para isso e deixe o compilador fazer todo o trabalho sujo. Isso geralmente significa encapsular recursos em seu próprio objeto membro. Você poderia nos mostrar algum código. Talvez tenhamos algumas boas sugestões de design.
sellibitze
2
Possível duplicata de Reduzindo a
mpromonet

Respostas:

121

Sim. Existem duas opções comuns. Uma - que geralmente é desencorajada - é chamar operator=explicitamente o do construtor de cópia:

MyClass(const MyClass& other)
{
    operator=(other);
}

No entanto, fornecer um bem operator=é um desafio quando se trata de lidar com o antigo estado e com as questões decorrentes da auto-atribuição. Além disso, todos os membros e bases são inicializados por padrão primeiro, mesmo se eles forem atribuídos a from other. Isso pode não ser válido para todos os membros e bases e, mesmo quando é válido, é semanticamente redundante e pode ser praticamente caro.

Uma solução cada vez mais popular é implementar operator=usando o construtor de cópia e um método de troca.

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

ou mesmo:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

Uma swapfunção normalmente é simples de escrever, pois apenas troca a propriedade dos componentes internos e não precisa limpar o estado existente ou alocar novos recursos.

As vantagens do idioma de cópia e troca é que ele é automaticamente seguro para auto-atribuição e - contanto que a operação de troca seja ilimitada - também é fortemente seguro para exceções.

Para ser totalmente seguro contra exceções, um operador de atribuição escrita 'à mão' normalmente tem que alocar uma cópia dos novos recursos antes de desalocar os recursos antigos do cessionário, de modo que se ocorrer uma exceção ao alocar os novos recursos, o estado antigo ainda pode ser devolvido para . Tudo isso vem de graça com o copy-and-swap, mas normalmente é mais complexo e, portanto, sujeito a erros para fazer do zero.

A única coisa a se ter cuidado é ter certeza de que o método swap é um swap verdadeiro, e não o padrão std::swapque usa o próprio construtor de cópia e operador de atribuição.

Normalmente, um memberwise swapé usado. std::swapfunciona e é 'no-throw' garantido com todos os tipos básicos e tipos de ponteiro. A maioria dos ponteiros inteligentes também pode ser trocada com uma garantia de não lançamento.

CB Bailey
fonte
3
Na verdade, não são operações comuns. Enquanto o ctor de cópia inicializa pela primeira vez os membros do objeto, o operador de atribuição substitui os valores existentes. Considerando isso, alling operator=do ctor de cópia é na verdade muito ruim, porque primeiro inicializa todos os valores para algum padrão apenas para substituí-los pelos valores do outro objeto logo em seguida.
sbi
14
Talvez em "Eu não recomendo", adicione "e nenhum especialista em C ++". Alguém pode aparecer e não perceber que você não está apenas expressando uma preferência pessoal de uma minoria, mas a opinião consensual estabelecida daqueles que realmente pensaram a respeito. E, OK, talvez eu esteja errado e algum especialista em C ++ o recomende, mas pessoalmente eu ainda daria o desafio para que alguém surgisse com uma referência para essa recomendação.
Steve Jessop
4
Justo, eu já votei contra você de qualquer maneira :-). Eu acho que se algo é amplamente considerado a melhor prática, então é melhor dizer isso (e olhar novamente se alguém disser que não é realmente o melhor, afinal). Da mesma forma, se alguém perguntasse "é possível usar mutexes em C ++", eu não diria "uma opção bastante comum é ignorar completamente o RAII e escrever código não seguro para exceções que bloqueia na produção, mas é cada vez mais popular para escrever código decente e funcional ";-)
Steve Jessop
4
+1. E acho que sempre há necessidade de análise. Acho que é razoável ter uma assignfunção de membro usada tanto pelo ctor de cópia quanto pelo operador de atribuição em alguns casos (para classes leves). Em outros casos (uso intensivo de recursos / casos de uso, identificador / corpo), uma cópia / troca é o caminho a seguir, é claro.
Johannes Schaub - litb
2
@litb: Fiquei surpreso com isso, então pesquisei o Item 41 na Exception C ++ (no qual isso se transformou) e essa recomendação em particular foi eliminada e ele recomenda copiar e trocar em seu lugar. Um tanto sorrateiramente, ele abandonou o "Problema nº 4: é ineficiente para atribuição" ao mesmo tempo.
CB Bailey
13

O construtor de cópia executa a inicialização inicial de objetos que costumavam ser memória bruta. O operador de atribuição, OTOH, substitui os valores existentes por novos. Mais frequentemente do que nunca, isso envolve descartar recursos antigos (por exemplo, memória) e alocar novos.

Se houver uma semelhança entre os dois, é que o operador de atribuição executa a destruição e a construção da cópia. Alguns desenvolvedores costumavam realmente implementar a atribuição por destruição no local seguida por construção de cópia de posicionamento. No entanto, essa é uma ideia muito ruim. (E se este for o operador de atribuição de uma classe base que chamou durante a atribuição de uma classe derivada?)

O que normalmente é considerado o idioma canônico hoje em dia está usando swapcomo Charles sugeriu:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

Este usa construção de cópia (note que otheré copiado) e destruição (é destruído no final da função) - e também os usa na ordem certa: construção (pode falhar) antes da destruição (não deve falhar).

sbi
fonte
Deve swapser declarado virtual?
1
@Johannes: Funções virtuais são usadas em hierarquias de classes polimórficas. Operadores de atribuição são usados ​​para tipos de valor. Os dois dificilmente se misturam.
sbi
-3

Algo me incomoda sobre:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

Primeiro, ler a palavra “trocar” quando minha mente está pensando em “copiar” irrita meu bom senso. Além disso, questiono o objetivo desse truque sofisticado. Sim, quaisquer exceções na construção dos novos recursos (copiados) devem acontecer antes da troca, o que parece uma maneira segura de garantir que todos os novos dados sejam preenchidos antes de colocá-los no ar.

Isso é bom. Então, e as exceções que acontecem após a troca? (quando os recursos antigos são destruídos quando o objeto temporário sai do escopo) Da perspectiva do usuário da atribuição, a operação falhou, exceto que não. Isso tem um grande efeito colateral: a cópia realmente aconteceu. Foi apenas alguma limpeza de recursos que falhou. O estado do objeto de destino foi alterado, embora a operação pareça ter falhado do lado de fora.

Portanto, proponho em vez de "trocar" para fazer uma "transferência" mais natural:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    transfer(tmp);
    return *this;
}

Ainda há a construção do objeto temporário, mas a próxima ação imediata é liberar todos os recursos atuais do destino antes de mover (e anular para que não sejam liberados duas vezes) os recursos da origem para ele.

Em vez de {construir, mover, destruir}, proponho {construir, destruir, mover}. O movimento, que é a ação mais perigosa, é o último executado depois que todo o resto foi resolvido.

Sim, a falha de destruição é um problema em qualquer esquema. Os dados estão corrompidos (copiados quando você não pensava que eram) ou perdidos (liberados quando você não pensa que estão). Perdido é melhor do que corrompido. Nenhum dado é melhor do que dados ruins.

Transfira em vez de trocar. Essa é a minha sugestão de qualquer maneira.

Mateus
fonte
2
Um destruidor não deve falhar, portanto, exceções na destruição não são esperadas. E eu não entendo qual seria a vantagem de mover o movimento atrás da destruição, se o movimento é a operação mais perigosa? Ou seja, no esquema padrão, uma falha de movimentação não corromperá o estado antigo, ao passo que seu novo esquema sim. Então por que? Além disso, First, reading the word "swap" when my mind is thinking "copy" irritates-> Como um escritor de biblioteca, você geralmente conhece as práticas comuns (cópia + troca), e o ponto crucial é my mind. Sua mente está realmente escondida atrás da interface pública. É disso que se trata o código reutilizável.
Sebastian Mach