Qual é esse idioma e quando deve ser usado? Quais problemas ele resolve? O idioma muda quando o C ++ 11 é usado?
Embora tenha sido mencionado em muitos lugares, não tivemos nenhuma pergunta e resposta singular "o que é isso", então aqui está. Aqui está uma lista parcial dos lugares onde foi mencionado anteriormente:
Respostas:
Visão geral
Por que precisamos do idioma de copiar e trocar?
Qualquer classe que gerencia um recurso (um invólucro , como um ponteiro inteligente) precisa implementar os Três Grandes . Embora os objetivos e a implementação do construtor e destruidor de cópias sejam diretos, o operador de atribuição de cópias é sem dúvida o mais detalhado e difícil. Como isso deve ser feito? Que armadilhas precisam ser evitadas?
O idioma de copiar e trocar é a solução e ajuda o operador de atribuição de maneira elegante a conseguir duas coisas: evitar a duplicação de código e fornecer uma forte garantia de exceção .
Como funciona?
Conceitualmente , ele funciona usando a funcionalidade do construtor de cópia para criar uma cópia local dos dados e, em seguida, leva os dados copiados com uma
swap
função, trocando os dados antigos pelos novos. A cópia temporária é destruída, levando os dados antigos. Ficamos com uma cópia dos novos dados.Para usar o idioma copy-and-swap, precisamos de três coisas: um construtor de cópias de trabalho, um destruidor de trabalho (ambos são a base de qualquer wrapper, portanto, deve estar completo de qualquer maneira) e uma
swap
função.Uma função de troca é uma função não lançadora que troca dois objetos de uma classe, membro por membro. Podemos ser tentados a usar, em
std::swap
vez de fornecer os nossos, mas isso seria impossível;std::swap
usa o operador construtor de cópia e atribuição de cópia em sua implementação e, finalmente, tentaríamos definir o operador de atribuição em termos de si mesmo!(Não apenas isso, mas chamadas não qualificadas para
swap
usar nosso operador de troca personalizado, ignorando a construção e destruição desnecessárias de nossa classe questd::swap
isso implicaria.)Uma explicação detalhada
O objetivo
Vamos considerar um caso concreto. Queremos gerenciar, em uma classe inútil, uma matriz dinâmica. Começamos com um construtor, copiador-construtor e destruidor:
Essa classe quase gerencia a matriz com êxito, mas precisa
operator=
funcionar corretamente.Uma solução com falha
Aqui está como uma implementação ingênua pode parecer:
E dizemos que terminamos; isso agora gerencia uma matriz, sem vazamentos. No entanto, ele sofre de três problemas, marcados sequencialmente no código como
(n)
.O primeiro é o teste de auto-atribuição. Essa verificação serve para dois propósitos: é uma maneira fácil de impedir a execução de códigos desnecessários na atribuição automática e nos protege de erros sutis (como excluir a matriz apenas para tentar copiá-la). Mas em todos os outros casos, serve apenas para retardar o programa e agir como ruído no código; a atribuição automática raramente ocorre; portanto, na maioria das vezes essa verificação é um desperdício. Seria melhor se o operador pudesse funcionar corretamente sem ele.
A segunda é que ela fornece apenas uma garantia básica de exceção. Se
new int[mSize]
falhar,*this
terá sido modificado. (Ou seja, o tamanho está incorreto e os dados se foram!) Para uma garantia de exceção forte, seria necessário algo semelhante a:O código foi expandido! O que nos leva ao terceiro problema: duplicação de código. Nosso operador de atribuição duplica efetivamente todo o código que já escrevemos em outro lugar, e isso é uma coisa terrível.
No nosso caso, o núcleo é de apenas duas linhas (a alocação e a cópia), mas com recursos mais complexos, esse inchaço do código pode ser um aborrecimento. Devemos nos esforçar para nunca nos repetir.
(Alguém pode se perguntar: se esse código é necessário para gerenciar um recurso corretamente, e se minha classe gerencia mais de um? Embora isso possa parecer uma preocupação válida e, de fato, exija cláusulas
try
/ não triviaiscatch
, isso não é Isso porque uma classe deve gerenciar apenas um recurso !)Uma solução de sucesso
Como mencionado, o idioma de copiar e trocar irá corrigir todos esses problemas. Mas agora, temos todos os requisitos, exceto um: uma
swap
função. Embora a Regra dos Três implique com sucesso a existência de nosso construtor de cópias, operador de atribuição e destruidor, ela deve realmente ser chamada de "Os Três Grandes e Meio": sempre que sua classe gerencia um recurso, também faz sentido fornecer umaswap
função .Precisamos adicionar a funcionalidade de troca à nossa classe e fazemos o seguinte:
( Aqui está a explicação do motivo
public friend swap
.) Agora, não apenas podemos trocar os nossosdumb_array
, mas os swaps em geral podem ser mais eficientes; apenas troca ponteiros e tamanhos, em vez de alocar e copiar matrizes inteiras. Além desse bônus em funcionalidade e eficiência, agora estamos prontos para implementar o idioma de copiar e trocar.Sem mais delongas, nosso operador de atribuição é:
E é isso! De uma só vez, todos os três problemas são resolvidos de uma maneira elegante.
Por que isso funciona?
Primeiro notamos uma escolha importante: o argumento do parâmetro é tomado por valor . Embora alguém possa facilmente fazer o seguinte (e, de fato, muitas implementações ingênuas do idioma):
Perdemos uma importante oportunidade de otimização . Não apenas isso, mas essa opção é crítica no C ++ 11, que será discutido mais adiante. (Em uma observação geral, uma orientação extremamente útil é a seguinte: se você deseja fazer uma cópia de algo em uma função, deixe o compilador fazer isso na lista de parâmetros.)
De qualquer forma, esse método de obter nosso recurso é a chave para eliminar a duplicação de código: usamos o código do construtor de cópia para fazer a cópia e nunca precisamos repetir nada disso. Agora que a cópia foi feita, estamos prontos para trocar.
Observe que, ao entrar na função, todos os novos dados já estão alocados, copiados e prontos para serem usados. É isso que nos dá uma forte garantia de exceção de graça: nem entraremos na função se a construção da cópia falhar e, portanto, não é possível alterar o estado de
*this
. (O que fizemos manualmente antes para garantir uma exceção forte, o compilador está fazendo por nós agora; que tipo.)Neste ponto, estamos livres de casa, porque
swap
não jogam. Trocamos nossos dados atuais pelos dados copiados, alterando com segurança nosso estado, e os dados antigos são colocados no temporário. Os dados antigos são liberados quando a função retorna. (Onde o escopo do parâmetro termina e seu destruidor é chamado.)Como o idioma não repete nenhum código, não podemos introduzir bugs no operador. Observe que isso significa que estamos livres da necessidade de uma verificação de auto-atribuição, permitindo uma implementação única e uniforme de
operator=
. (Além disso, não temos mais uma penalidade de desempenho em não atribuições próprias.)E esse é o idioma de copiar e trocar.
E o C ++ 11?
A próxima versão do C ++, C ++ 11, faz uma mudança muito importante na maneira como gerenciamos os recursos: a Regra dos Três é agora a Regra dos Quatro (e meia). Por quê? Como não precisamos apenas copiar e construir nosso recurso, precisamos movê-lo também .
Felizmente para nós, isso é fácil:
O que está acontecendo aqui? Lembre-se do objetivo de mover-construção: pegar os recursos de outra instância da classe, deixando-a em um estado garantido para ser atribuível e destrutível.
Então, o que fizemos é simples: inicialize através do construtor padrão (um recurso do C ++ 11), depois troque com
other
; sabemos que uma instância construída padrão de nossa classe pode ser atribuída e destruída com segurança; portanto, sabemosother
que será capaz de fazer o mesmo após a troca.(Observe que alguns compiladores não oferecem suporte à delegação de construtores; nesse caso, temos que construir manualmente manualmente a classe. Essa é uma tarefa infeliz, mas felizmente trivial.)
Por que isso funciona?
Essa é a única mudança que precisamos fazer em nossa classe, então por que funciona? Lembre-se da sempre importante decisão que tomamos para tornar o parâmetro um valor e não uma referência:
Agora, se
other
estiver sendo inicializado com um rvalue, ele será construído com movimentos . Perfeito. Da mesma maneira que o C ++ 03 vamos reutilizar nossa funcionalidade de construtor de cópia, assumindo o argumento por valor, o C ++ 11 selecionará automaticamente o construtor de movimentação quando apropriado também. (E, é claro, como mencionado no artigo vinculado anteriormente, a cópia / movimentação do valor pode simplesmente ser totalmente eliminada.)E assim conclui o idioma de copiar e trocar.
Notas de rodapé
* Por que definimos
mArray
como nulo? Porque se qualquer código adicional no operador lançar, o destruidor dedumb_array
poderá ser chamado; e se isso acontecer sem defini-lo como nulo, tentamos excluir a memória que já foi excluída! Evitamos isso definindo-o como nulo, pois a exclusão de nulo é uma não operação.† Há outras alegações de que devemos nos especializar
std::swap
para o nosso tipo, fornecer umaswap
função gratuita ao lado da classeswap
, etc. Mas tudo isso é desnecessário: qualquer uso adequadoswap
será por meio de uma chamada não qualificada e nossa função será encontrado através de ADL . Uma função serve.‡ O motivo é simples: depois de ter o recurso disponível, você pode trocá-lo e / ou movê-lo (C ++ 11) para qualquer lugar que ele precisar. E, ao fazer a cópia na lista de parâmetros, você maximiza a otimização.
†† O construtor de movimentação geralmente deve ser
noexcept
, caso contrário, algum código (por exemplo,std::vector
lógica de redimensionamento) usará o construtor de cópia mesmo quando uma movimentação faria sentido. Obviamente, apenas marque-o como exceto se o código interno não gerar exceções.fonte
swap
ser encontrado durante o ADL, se quiser que ele funcione no código mais genérico que você encontrará, como emboost::swap
outras instâncias de troca. A troca é uma questão complicada em C ++, e geralmente todos concordamos que um único ponto de acesso é o melhor (por consistência), e a única maneira de fazer isso em geral é uma função livre (int
não pode ter um membro de troca, por exemplo). Veja minha pergunta para alguns antecedentes.A atribuição, em sua essência, é duas etapas: derrubar o antigo estado do objeto e construir seu novo estado como uma cópia do estado de outro objeto.
Basicamente, é isso que o destruidor e o construtor de cópias fazem, então a primeira idéia seria delegar o trabalho a eles. No entanto, como a destruição não deve falhar, enquanto a construção pode, na verdade, queremos fazer o contrário : primeiro execute a parte construtiva e, se tiver sido bem-sucedida, faça a parte destrutiva . O idioma de copiar e trocar é uma maneira de fazer exatamente isso: primeiro chama o construtor de cópias de uma classe para criar um objeto temporário, depois troca seus dados pelos do temporário e, em seguida, permite que o destruidor do temporário destrua o estado antigo.
Desde a
swap()
deve nunca falhar, a única parte que pode falhar é a construção da cópia. Isso é realizado primeiro e, se falhar, nada será alterado no objeto de destino.Em sua forma refinada, a cópia e troca é implementada pela execução da cópia, inicializando o parâmetro (sem referência) do operador de atribuição:
fonte
std::swap(this_string, that)
não oferece garantia de não-lance. Ele oferece uma forte segurança de exceção, mas não uma garantia de não-lance.std::string::swap
(que são chamadas porstd::swap
). No C ++ 0x,std::string::swap
énoexcept
e não deve lançar exceções.std::array
...)Já existem algumas boas respostas. Vou me concentrar principalmente no que acho que falta - uma explicação dos "contras" com o idioma de copiar e trocar ....
Uma maneira de implementar o operador de atribuição em termos de uma função de swap:
A ideia fundamental é que:
a parte mais propensa a erros de atribuir a um objeto é garantir que todos os recursos necessários para o novo estado sejam adquiridos (por exemplo, memória, descritores)
essa aquisição pode ser tentada antes de modificar o estado atual do objeto (ou seja
*this
) se uma cópia do novo valor for feita, e é por isso querhs
é aceito por valor (ou seja, copiado) e não por referênciatrocar o estado da cópia local
rhs
e geralmente*this
é relativamente fácil de fazer sem possíveis falhas / exceções, dado que a cópia local não precisa de nenhum estado específico posteriormente (apenas precisa do estado adequado para o destruidor executar, assim como para um objeto sendo movido de em> = C ++ 11)Quando você deseja que o objeto atribuído seja afetado por uma atribuição que gera uma exceção, supondo que você tenha ou possa escrever uma
swap
garantia com exceção forte e, idealmente, uma que não possa falhar /throw
.. †Quando você deseja uma maneira limpa, fácil de entender e robusta de definir o operador de atribuição em termos de
swap
funções (mais simples) de construtor de cópias e funções de destruidor.†
swap
lançamento: geralmente é possível trocar confiavelmente membros de dados que os objetos rastreiam por ponteiro, mas membros que não são de ponteiros que não possuem troca sem lançamento ou para os quais a troca deve ser implementada comoX tmp = lhs; lhs = rhs; rhs = tmp;
construção de cópia ou atribuição pode jogar, ainda tem o potencial de falhar, deixando alguns membros de dados trocados e outros não. Esse potencial se aplica até ao C ++ 03std::string
, quando James comenta outra resposta:‡ A implementação do operador de atribuição que parece sã ao atribuir a partir de um objeto distinto pode facilmente falhar na atribuição automática. Embora possa parecer inimaginável que o código do cliente tente tentar se auto-atribuir, isso pode acontecer com relativa facilidade durante operações de algo em contêineres, com
x = f(x);
código em quef
é (talvez apenas para algumas#ifdef
ramificações) uma ala de macro#define f(x) x
ou uma função que retorna uma referênciax
ou até mesmo (provavelmente ineficiente, mas conciso) comox = c1 ? x * 2 : c2 ? x / 2 : x;
). Por exemplo:Na auto-atribuição, a exclusão do código acima
x.p_;
apontap_
para uma região de heap recém-alocada e tenta ler os dados não inicializados nela (comportamento indefinido), se isso não fizer algo muito estranho,copy
tenta uma auto-atribuição para todos os destruído 'T'!Id O idioma de copiar e trocar pode introduzir ineficiências ou limitações devido ao uso de um temporário extra (quando o parâmetro do operador é construído com cópia):
Aqui, um manuscrito
Client::operator=
pode verificar se*this
já está conectado ao mesmo servidor querhs
(talvez enviando um código de "redefinição", se útil), enquanto a abordagem de copiar e trocar invocaria o construtor de cópia que provavelmente seria gravado para abrir uma conexão de soquete distinta e feche a original. Isso poderia significar não apenas uma interação remota de rede, em vez de uma cópia simples de variável em processo, mas também os limites de clientes ou servidores em recursos ou conexões de soquete. (É claro que essa classe tem uma interface bastante horrível, mas isso é outra questão ;-P).fonte
Client
é que a atribuição não é proibida.Esta resposta é mais como uma adição e uma ligeira modificação às respostas acima.
Em algumas versões do Visual Studio (e possivelmente em outros compiladores), há um bug que é realmente irritante e não faz sentido. Portanto, se você declarar / definir sua
swap
função assim:... o compilador gritará com você quando você chamar a
swap
função:Isso tem algo a ver com uma
friend
função sendo chamada e umthis
objeto sendo passado como parâmetro.Uma maneira de contornar isso é não usar a
friend
palavra-chave e redefinir aswap
função:Desta vez, você pode simplesmente ligar
swap
e passarother
, deixando o compilador feliz:Afinal, você não precisa usar uma
friend
função para trocar 2 objetos. Faz tanto sentido fazerswap
uma função membro que tenha umother
objeto como parâmetro.Você já tem acesso ao
this
objeto, portanto, transmiti-lo como um parâmetro é tecnicamente redundante.fonte
friend
função é chamada com*this
o parâmetroGostaria de adicionar uma palavra de aviso ao lidar com contêineres compatíveis com o alocador do estilo C ++ 11. A troca e a atribuição têm semânticas sutilmente diferentes.
Para concretização, vamos considerar um contêiner
std::vector<T, A>
, ondeA
é algum tipo de alocador com estado, e compararemos as seguintes funções:O objetivo de ambas as funções
fs
efm
é dara
o estado queb
tinha inicialmente. No entanto, há uma pergunta oculta: o que acontece sea.get_allocator() != b.get_allocator()
? A resposta é: depende. Vamos gravaçãoAT = std::allocator_traits<A>
.Se
AT::propagate_on_container_move_assignment
estiverstd::true_type
,fm
reatribui o alocador dea
com o valor deb.get_allocator()
, caso contrário, não ea
continua a usar seu alocador original. Nesse caso, os elementos de dados precisam ser trocados individualmente, pois o armazenamentoa
eb
não é compatível.Se
AT::propagate_on_container_swap
estiverstd::true_type
,fs
troque dados e alocadores da maneira esperada.Se
AT::propagate_on_container_swap
forstd::false_type
, precisamos de uma verificação dinâmica.a.get_allocator() == b.get_allocator()
, então os dois contêineres usam armazenamento compatível, e a troca prossegue da maneira usual.a.get_allocator() != b.get_allocator()
o programa tiver um comportamento indefinido (consulte [container.requirements.general / 8]).O resultado é que a troca se tornou uma operação não trivial no C ++ 11 assim que seu contêiner começa a oferecer suporte a alocadores com estado. Esse é um "caso de uso avançado", mas não é totalmente improvável, pois as otimizações de movimento geralmente só se tornam interessantes quando a classe gerencia um recurso, e a memória é um dos recursos mais populares.
fonte