No operador de atribuição de uma classe, você geralmente precisa verificar se o objeto que está sendo atribuído é o objeto de chamada para não estragar tudo:
Class& Class::operator=(const Class& rhs) {
if (this != &rhs) {
// do the assignment
}
return *this;
}
Você precisa da mesma coisa para o operador de atribuição de movimentação? Existe alguma situação em que this == &rhs
isso seria verdade?
? Class::operator=(Class&& rhs) {
?
}
c++
c++11
move-semantics
move-assignment-operator
Seth Carnegie
fonte
fonte
A a; a = std::move(a);
.std::move
é normal. Depois, leve em consideração o alias e, quando você estiver dentro de uma pilha de chamadas e tiver uma referênciaT
e outra referência aT
... você verificará a identidade aqui? Deseja encontrar a primeira chamada (ou chamadas) em que a documentação de que você não pode passar o mesmo argumento duas vezes provará estaticamente que essas duas referências não terão alias? Ou você fará a auto-atribuição funcionar?std::sort
ou astd::shuffle
qualquer momento que você estiver trocando osi
th ej
th th elementos de uma matriz sem verificari != j
primeiro. (std::swap
É implementado em termos de atribuição de movimento.)Respostas:
Uau, há tanta coisa para limpar aqui ...
Primeiro, a cópia e troca nem sempre é a maneira correta de implementar a atribuição de cópias. Quase certamente no caso de
dumb_array
, esta é uma solução subótima.O uso de Copy and Swap é
dumb_array
um exemplo clássico de colocar a operação mais cara com os recursos mais completos na camada inferior. É perfeito para clientes que desejam o recurso mais completo e estão dispostos a pagar a penalidade de desempenho. Eles conseguem exatamente o que querem.Mas é desastroso para os clientes que não precisam do recurso mais completo e procuram o melhor desempenho. Para eles,
dumb_array
é apenas mais um software que eles precisam reescrever, porque é muito lento. Se tivessedumb_array
sido projetado de maneira diferente, poderia ter satisfeito os dois clientes sem comprometer os dois.A chave para satisfazer os dois clientes é criar as operações mais rápidas no nível mais baixo e, em seguida, adicionar a API para obter recursos mais completos com mais custos. Ou seja, você precisa da garantia de exceção forte, tudo bem, você paga por isso. Você não precisa disso? Aqui está uma solução mais rápida.
Vamos ser concretos: Aqui está o operador rápido e básico de garantia de exceção para Copy Assignment para
dumb_array
:Explicação:
Uma das coisas mais caras que você pode fazer no hardware moderno é fazer uma viagem para a pilha. Tudo o que você pode fazer para evitar uma viagem para a pilha é tempo e esforço bem gastos. Clientes de
dumb_array
podem muito bem querer atribuir matrizes do mesmo tamanho. E quando o fazem, tudo o que você precisa fazer é ummemcpy
(oculto abaixostd::copy
). Você não deseja alocar uma nova matriz do mesmo tamanho e desalocar a antiga do mesmo tamanho!Agora, para seus clientes que realmente desejam uma forte segurança de exceção:
Ou talvez, se você quiser tirar proveito da atribuição de movimentação no C ++ 11, deve ser:
Se
dumb_array
os clientes valorizam a velocidade, eles devem chamar ooperator=
. Se eles precisam de segurança de exceção forte, existem algoritmos genéricos que eles podem chamar que funcionarão em uma ampla variedade de objetos e precisam ser implementados apenas uma vez.Agora, de volta à pergunta original (que tem um tipo O neste momento):
Esta é realmente uma questão controversa. Alguns dirão que sim, absolutamente, outros dirão que não.
Minha opinião pessoal é não, você não precisa dessa verificação.
Fundamentação:
Quando um objeto se liga a uma referência rvalue, é uma das duas coisas:
Se você tiver uma referência a um objeto que é temporário real, então, por definição, você terá uma referência exclusiva a esse objeto. Não pode ser referenciado por nenhum outro lugar em todo o programa. Ou
this == &temporary
seja, não é possível .Agora, se seu cliente mentiu para você e prometeu que você seria temporário quando não estiver, é responsabilidade do cliente garantir que você não precise se preocupar. Se você quer ser realmente cuidadoso, acredito que essa seria uma implementação melhor:
Ou seja, se você está passado uma referência auto, este é um erro por parte do cliente que deve ser corrigido.
Para completar, eis um operador de atribuição de movimento para
dumb_array
:No caso de uso típico da atribuição de movimentação,
*this
será um objeto movido de e, portanto,delete [] mArray;
deve ser um não operacional. É fundamental que as implementações excluam o nullptr o mais rápido possível.Embargo:
Alguns argumentam que
swap(x, x)
é uma boa ideia ou apenas um mal necessário. E isso, se a troca for para a troca padrão, poderá causar uma atribuição de movimentação automática.Não concordo que
swap(x, x)
seja sempre uma boa ideia. Se encontrado em meu próprio código, considerarei um bug de desempenho e o corrigirei. Mas, caso você queira permitir, perceba queswap(x, x)
apenas a auto-move-assignemnet em um valor movido de. E, no nossodumb_array
exemplo, isso será perfeitamente inofensivo se simplesmente omitirmos a afirmação ou restringirmos ao caso que mudou de:Se você atribuir automaticamente dois objetos movidos (vazios)
dumb_array
, não fará nada de errado, além de inserir instruções inúteis em seu programa. Essa mesma observação pode ser feita para a grande maioria dos objetos.<
Atualizar>
Pensei mais sobre esse assunto e mudei de posição um pouco. Agora acredito que a atribuição deve tolerar a auto-atribuição, mas que as condições de postagem na atribuição de cópias e na movimentação são diferentes:
Para atribuição de cópia:
deve-se ter uma pós-condição na qual o valor de
y
não deve ser alterado. Quando,&x == &y
então, essa pós-condição se traduzirá em: a atribuição de autocópia não deve afetar o valor dex
.Para atribuição de movimento:
deve-se ter uma pós-condição
y
com um estado válido, mas não especificado. Quando,&x == &y
então, essa pós-condição se traduz em:x
tem um estado válido, mas não especificado. Ou seja, a atribuição de movimentação automática não precisa ser um não-op. Mas não deve falhar. Esta pós-condição é consistente com a permissãoswap(x, x)
para apenas trabalhar:O acima funciona, contanto
x = std::move(x)
que não trava. Pode sairx
em qualquer estado válido, mas não especificado.Vejo três maneiras de programar o operador de atribuição de movimentação
dumb_array
para conseguir isso:A implementação acima tolera auto atribuição, mas
*this
eother
acabar sendo uma matriz de tamanho zero após a atribuição auto-movimento, não importa o que o valor original*this
é. Isto é bom.A implementação acima tolera a atribuição automática da mesma maneira que o operador de atribuição de cópia, tornando-o não operacional. Isso também está bom.
O acima exposto está ok somente se
dumb_array
não reter recursos que devem ser destruídos "imediatamente". Por exemplo, se o único recurso é memória, o acima é bom. Se fordumb_array
possível reter bloqueios mutex ou o estado aberto dos arquivos, o cliente poderá razoavelmente esperar que esses recursos nos lhs da atribuição de movimentação sejam liberados imediatamente e, portanto, essa implementação pode ser problemática.O custo do primeiro é de duas lojas extras. O custo do segundo é um teste e ramificação. Ambos funcionam. Ambos atendem a todos os requisitos dos requisitos da Tabela 22 MoveAssignable no padrão C ++ 11. O terceiro também funciona com o módulo não-memória-recurso-preocupação.
Todas as três implementações podem ter custos diferentes, dependendo do hardware: Qual o custo de uma filial? Existem muitos registros ou muito poucos?
A conclusão é que a atribuição de movimentação automática, diferentemente da atribuição de cópia automática, não precisa preservar o valor atual.
<
/Atualizar>
Uma edição final (espero) inspirada no comentário de Luc Danton:
Se você estiver escrevendo uma classe de alto nível que não gerencia diretamente a memória (mas pode ter bases ou membros que o fazem), a melhor implementação da atribuição de movimentação é geralmente:
Isso moverá a atribuição de cada base e cada membro, por sua vez, e não incluirá um
this != &other
cheque. Isso lhe dará o desempenho mais alto e a segurança básica de exceções, assumindo que nenhum invariável precise ser mantido entre suas bases e membros. Para seus clientes que exigem uma forte segurança de exceção, aponte-os parastrong_assign
.fonte
std::swap(x,x)
, por que devo confiar nela para lidar com operações mais complicadas corretamente?std::swap(x,x)
, ele funciona mesmo quandox = std::move(x)
produz um resultado não especificado. Tente! Você não precisa acreditar em mim.swap
funciona desde quex = move(x)
saiax
em qualquer estado passível de mudança. E os algoritmosstd::copy
/std::move
são definidos de modo a produzir um comportamento indefinido em cópias não operacionais (ai; o jovem de 20 anosmemmove
acerta o caso trivial, masstd::move
não o faz!). Acho que ainda não pensei em um "slam dunk" para auto-atribuição. Mas, obviamente, a auto-atribuição é algo que acontece muito no código real, independentemente de o Padrão o ter abençoado ou não.Primeiro, você errou na assinatura do operador de atribuição de movimento. Como as movimentações roubam recursos do objeto de origem, a fonte deve ser uma
const
referência sem valor r.Observe que você ainda retorna por uma referência de valor (não
const
) l .Para qualquer um dos tipos de atribuição direta, o padrão não é verificar a auto-atribuição, mas garantir que uma auto-atribuição não cause acidente e queime. Geralmente, ninguém faz
x = x
ouy = std::move(y)
chama explicitamente , mas o alias, especialmente por meio de várias funções, pode levara = b
ouc = std::move(d)
tornar-se auto-atribuições. Uma verificação explícita da atribuição automática, ou sejathis == &rhs
, que ignora a função da função quando verdadeira é uma maneira de garantir a segurança da atribuição automática. Mas é uma das piores maneiras, pois otimiza um caso (espero) raro, enquanto é uma anti-otimização para o caso mais comum (devido a ramificações e possivelmente falhas de cache).Agora, quando (pelo menos) um dos operandos for um objeto diretamente temporário, você nunca poderá ter um cenário de auto-atribuição. Algumas pessoas defendem a hipótese e otimizam tanto o código, que o código se torna estupidamente suicida quando a suposição está errada. Eu digo que despejar a verificação do mesmo objeto nos usuários é irresponsável. Não defendemos esse argumento para atribuição de cópias; por que inverter a posição da atribuição de movimento?
Vamos fazer um exemplo, alterado de outro entrevistado:
Esta atribuição de cópia lida com a atribuição automática sem uma verificação explícita. Se os tamanhos de origem e destino diferirem, a desalocação e a realocação precedem a cópia. Caso contrário, apenas a cópia é feita. A atribuição automática não obtém um caminho otimizado, é despejada no mesmo caminho de quando os tamanhos de origem e destino começam iguais. A cópia é tecnicamente desnecessária quando os dois objetos são equivalentes (inclusive quando são o mesmo objeto), mas esse é o preço quando não é feita uma verificação de igualdade (em termos de valor ou endereço), já que a verificação em si seria um desperdício do tempo. Observe que a auto-atribuição de objeto aqui causará uma série de auto-atribuições no nível do elemento; o tipo de elemento deve ser seguro para fazer isso.
Como seu exemplo de origem, essa atribuição de cópia fornece a garantia básica de segurança de exceções. Se você deseja uma garantia forte, use o operador de atribuição unificada da consulta Copiar e trocar original , que lida com a atribuição de cópia e movimentação. Mas o objetivo deste exemplo é reduzir a segurança em um nível para ganhar velocidade. (BTW, estamos assumindo que os valores dos elementos individuais são independentes; que não há restrições invariantes limitando alguns valores em comparação com outros).
Vejamos uma atribuição de movimento para esse mesmo tipo:
Um tipo de troca que precisa de personalização deve ter uma função livre de dois argumentos chamada
swap
no mesmo espaço de nome que o tipo. (A restrição de espaço para nome permite que chamadas não qualificadas sejam trocadas para funcionar.) Um tipo de contêiner também deve adicionar umaswap
função de membro pública para corresponder aos contêineres padrão. Se um membroswap
não for fornecido, a função livreswap
provavelmente precisará ser marcada como um amigo do tipo permutável. Se você personalizar as jogadas para usarswap
, precisará fornecer seu próprio código de troca; o código padrão chama o código de movimentação do tipo, o que resultaria em recursão mútua infinita para tipos personalizados de movimentação.Assim como os destruidores, as funções de troca e as operações de movimentação nunca devem ser lançadas, se possível, e provavelmente marcadas como tal (no C ++ 11). Os tipos e rotinas de biblioteca padrão têm otimizações para tipos móveis não lançáveis.
Esta primeira versão da atribuição de movimento cumpre o contrato básico. Os marcadores de recursos da fonte são transferidos para o objeto de destino. Os recursos antigos não serão vazados, já que o objeto de origem agora os gerencia. E o objeto de origem é deixado em um estado utilizável, onde outras operações, incluindo atribuição e destruição, podem ser aplicadas a ele.
Observe que essa atribuição de movimentação é automaticamente segura para atribuição automática, pois a
swap
chamada é. Também é fortemente seguro contra exceções. O problema é a retenção desnecessária de recursos. Conceitualmente, os recursos antigos para o destino não são mais necessários, mas aqui eles ainda estão disponíveis apenas para que o objeto de origem possa permanecer válido. Se a destruição agendada do objeto de origem estiver muito distante, estamos desperdiçando espaço de recursos, ou pior, se o espaço total de recursos for limitado e outras petições de recursos acontecerem antes que o (novo) objeto de origem morra oficialmente.Essa questão foi o que causou o controverso conselho atual do guru sobre a auto-segmentação durante a atribuição de movimento. A maneira de escrever a atribuição de movimento sem recursos remanescentes é algo como:
A fonte é redefinida para as condições padrão, enquanto os recursos de destino antigos são destruídos. No caso de auto-atribuição, seu objeto atual acaba cometendo suicídio. A principal maneira de contornar isso é cercar o código de ação com um
if(this != &other)
bloco ou danificá-lo e permitir que os clientes comam umaassert(this != &other)
linha inicial (se você estiver se sentindo bem).Uma alternativa é estudar como tornar a atribuição de cópia fortemente exceção segura, sem atribuição unificada e aplicá-la à atribuição de movimentação:
Quando
other
ethis
são distintos,other
é esvaziado pela mudançatemp
e permanece assim. Em seguida,this
perde seus recursos antigos paratemp
obter os recursos originalmente mantidos porother
. Então os antigos recursos dethis
são mortos quando otemp
fazem.Quando a auto-atribuição acontece, o esvaziamento do
other
quetemp
esvaziathis
bem. Em seguida, o objeto de destino recupera seus recursos quandotemp
ethis
troca. A morte detemp
reivindicações um objeto vazio, que deve ser praticamente um não-op. O objetothis
/other
mantém seus recursos.A atribuição de movimentação nunca deve ser lançada, contanto que a construção e a troca de movimentação também sejam. O custo de também estar seguro durante a atribuição automática é de mais algumas instruções sobre os tipos de baixo nível, que devem ser abafados pela chamada de desalocação.
fonte
delete
seu segundo bloco de código?std::copy
causa comportamento indefinido se os intervalos de origem e destino se sobrepõem (incluindo o caso quando coincidem). Veja C ++ 14 [alg.copy] / 3.Estou no campo daqueles que desejam operadores seguros de auto-atribuição, mas não querem escrever verificações de auto-atribuição nas implementações de
operator=
. E, de fato, eu nem quero implementaroperator=
, quero que o comportamento padrão funcione 'imediatamente'. Os melhores membros especiais são aqueles que vêm de graça.Dito isto, os requisitos MoveAssignable presentes na Norma são descritos a seguir (de 17.6.3.1 Requisitos de argumento do modelo [utility.arg.requirements], n3290):
onde os espaços reservados são descritos como: "
t
[é] um valor modificável do tipo T;" e "rv
é um rvalor do tipo T;". Observe que esses são requisitos colocados nos tipos usados como argumentos para os modelos da biblioteca Padrão, mas procurando em outro lugar no Padrão, noto que todos os requisitos na atribuição de movimentação são semelhantes a este.Isso significa que
a = std::move(a)
deve ser "seguro". Se o que você precisa é de um teste de identidade (por exemplothis != &other
), faça-o, caso contrário você não poderá colocar seus objetosstd::vector
! (A menos que você não use os membros / operações que exigem MoveAssignable; mas não se importe com isso.) Observe que no exemplo anteriora = std::move(a)
, elethis == &other
realmente será válido.fonte
a = std::move(a)
não funcionamento faria uma classe não funcionarstd::vector
? Exemplo?std::vector<T>::erase
não é permitida, a menos queT
seja MoveAssignable. (Além do IIRC, alguns requisitos do MoveAssignable foram flexibilizados para o MoveInsertable em vez do C ++ 14.)T
deve ser MoveAssignable, mas por queerase()
dependeria de mover um elemento para si mesmo ?Conforme sua
operator=
função atual é escrita, desde que você criou o argumento rvalue-referenceconst
, não há como "roubar" os ponteiros e alterar os valores da referência de entrada rvalue ... você simplesmente não pode alterá-lo. só podia ler a partir dele. Eu só veria um problema se você começasse a chamardelete
ponteiros, etc. em seuthis
objeto, como faria em umoperator=
método normal de referência de lvaue , mas isso meio que derrota o ponto da versão rvalue ... ou seja, parece redundante usar a versão rvalue para basicamente fazer as mesmas operações normalmente deixadas no métodoconst
-lvalueoperator=
.Agora, se você definiu
operator=
paraconst
usar uma referência sem valor, a única maneira pela qual pude ver uma verificação ser necessária era se você passasse othis
objeto para uma função que intencionalmente retornasse uma referência a valor em vez de temporária.Por exemplo, suponha que alguém tentou escrever uma
operator+
função e utilize uma combinação de referências rvalue e lvalue para "impedir" que temporários extras sejam criados durante alguma operação de adição empilhada no tipo de objeto:Agora, pelo que entendi sobre as referências rvalue, fazer o acima é desencorajado (ou seja, você deve retornar uma referência temporária, e não rvalue), mas, se alguém ainda fizer isso, convém verificar verifique se a referência de valor-entrada recebida não estava referenciando o mesmo objeto que o
this
ponteiro.fonte
const
, você poderá ler apenas nela, portanto, a única necessidade é fazer uma verificação seria se você decidisseoperator=(const T&&)
executar a mesma reinicialização dothis
que faria em umoperator=(const T&)
método típico , em vez de uma operação no estilo de troca (por exemplo, roubar ponteiros etc. em vez de fazer cópias profundas).Minha resposta ainda é que a atribuição de movimento não precisa ser salva contra a auto-atribuição, mas tem uma explicação diferente. Considere std :: unique_ptr. Se eu fosse implementar um, faria algo assim:
Se você olhar para Scott Meyers explicando isso, ele faz algo semelhante. (Se você vagar por que não fazer swap - ele tem uma gravação extra). E isso não é seguro para auto-atribuição.
Às vezes isso é lamentável. Considere sair do vetor todos os números pares:
Isso é bom para números inteiros, mas não acredito que você possa fazer algo assim funcionar com a semântica de movimentação.
Para concluir: mover a atribuição para o objeto em si não está ok e você deve estar atento.
Pequena atualização.
swap(x, x)
deve funcionar. Os algoritmos adoram essas coisas! É sempre bom quando uma caixa de esquina simplesmente funciona. (E ainda estou para ver um caso em que não é de graça. Não significa que não exista).unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...}
É seguro para a atribuição de movimentação automática.fonte
Existe uma situação em que (isso == rhs) eu consigo pensar. Para esta afirmação: Myclass obj; std :: move (obj) = std :: move (obj)
fonte