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 tivesse dumb_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
:
dumb_array& operator=(const dumb_array& other)
{
if (this != &other)
{
if (mSize != other.mSize)
{
delete [] mArray;
mArray = nullptr;
mArray = other.mSize ? new int[other.mSize] : nullptr;
mSize = other.mSize;
}
std::copy(other.mArray, other.mArray + mSize, mArray);
}
return *this;
}
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 é um memcpy
(oculto abaixo std::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:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
swap(lhs, rhs);
return lhs;
}
Ou talvez, se você quiser tirar proveito da atribuição de movimentação no C ++ 11, deve ser:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
lhs = std::move(rhs);
return lhs;
}
Se dumb_array
os clientes valorizam a velocidade, eles devem chamar o operator=
. 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):
Class&
Class::operator=(Class&& rhs)
{
if (this == &rhs) // is this check needed?
{
// ...
}
return *this;
}
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:
- Um temporário.
- Um objeto que o chamador deseja que você acredite ser temporário.
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:
Class&
Class::operator=(Class&& other)
{
assert(this != &other);
// ...
return *this;
}
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
:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
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 que swap(x, x)
apenas a auto-move-assignemnet em um valor movido de. E, no nosso dumb_array
exemplo, isso será perfeitamente inofensivo se simplesmente omitirmos a afirmação ou restringirmos ao caso que mudou de:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other || mSize == 0);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
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:
x = y;
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 de x
.
Para atribuição de movimento:
x = std::move(y);
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ão swap(x, x)
para apenas trabalhar:
template <class T>
void
swap(T& x, T& y)
{
// assume &x == &y
T tmp(std::move(x));
// x and y now have a valid but unspecified state
x = std::move(y);
// x and y still have a valid but unspecified state
y = std::move(tmp);
// x and y have the value of tmp, which is the value they had on entry
}
O acima funciona, contanto x = std::move(x)
que não trava. Pode sair x
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:
dumb_array& operator=(dumb_array&& other)
{
delete [] mArray;
// set *this to a valid state before continuing
mSize = 0;
mArray = nullptr;
// *this is now in a valid state, continue with move assignment
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
A implementação acima tolera auto atribuição, mas *this
e other
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.
dumb_array& operator=(dumb_array&& other)
{
if (this != &other)
{
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
}
return *this;
}
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.
dumb_array& operator=(dumb_array&& other)
{
swap(other);
return *this;
}
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 for dumb_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:
Class& operator=(Class&&) = default;
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 para strong_assign
.