Mova o operador de atribuição e `if (this! = & Rhs)`

125

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 == &rhsisso seria verdade?

? Class::operator=(Class&& rhs) {
    ?
}
Seth Carnegie
fonte
12
Irrelevante para o Q ser solicitado, e apenas para que os novos usuários que leem este Q na linha do tempo (pelo que Seth já sabe disso) não tenham idéias erradas, o Copy and Swap é a maneira correta de implementar o Operador de atribuição de cópia no qual você não precisa verificar a auto-atribuição e tudo.
Alok Salvar
5
@VaughnCato: A a; a = std::move(a);.
Xeo 17/02/12
11
@VaughnCato O uso std::moveé normal. Depois, leve em consideração o alias e, quando você estiver dentro de uma pilha de chamadas e tiver uma referência Te outra referência a T... 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?
Luc Danton
2
@ LucDanton Eu preferiria uma afirmação no operador de atribuição. Se std :: move fosse usado de tal maneira que fosse possível terminar com uma auto-atribuição rvalue, eu consideraria um bug que deveria ser corrigido.
Vaughn Cato
4
@VaughnCato Um lugar em que a troca automática é normal é dentro std::sortou a std::shufflequalquer momento que você estiver trocando os ith e jth th elementos de uma matriz sem verificar i != jprimeiro. ( std::swapÉ implementado em termos de atribuição de movimento.)
Quuxplusone

Respostas:

143

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_arrayum 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_arraysido 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_arraypodem 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_arrayos 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:

  1. Um temporário.
  2. 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, *thisserá 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_arrayexemplo, 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 ynão deve ser alterado. Quando, &x == &yentã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 ycom um estado válido, mas não especificado. Quando, &x == &yentão, essa pós-condição se traduz em: xtem 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 xem qualquer estado válido, mas não especificado.

Vejo três maneiras de programar o operador de atribuição de movimentação dumb_arraypara 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 *thise otheracabar 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_arraynão reter recursos que devem ser destruídos "imediatamente". Por exemplo, se o único recurso é memória, o acima é bom. Se for dumb_arraypossí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 != &othercheque. 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.

Howard Hinnant
fonte
6
Não sei como me sentir sobre essa resposta. Parece que implementar essas classes (que gerenciam sua memória muito explicitamente) é uma coisa comum a se fazer. É verdade que quando você fazer write tal classe a pessoa tem que ser muito muito cuidado com garantias de segurança de exceção e encontrar o ponto ideal para a interface para ser conciso, mas conveniente, mas a questão parece estar pedindo conselhos em geral.
21812 Luc Danton
Sim, eu definitivamente nunca uso copiar e trocar porque é um desperdício de tempo para aulas que gerenciam recursos e coisas (por que fazer outra cópia inteira de todos os seus dados?). E obrigado, isso responde à minha pergunta.
Seth Carnegie
5
Votado com a sugestão de que a atribuição de movimento de si mesmo deve sempre afirmar que falhou ou produzir um resultado "não especificado". A atribuição de si é literalmente o caso mais fácil de acertar. Se sua classe falhar std::swap(x,x), por que devo confiar nela para lidar com operações mais complicadas corretamente?
Quuxplusone
1
@ Quuxplusone: cheguei a um acordo com você sobre a afirmação de falha, conforme observado na atualização da minha resposta. Na medida do possível std::swap(x,x), ele funciona mesmo quando x = std::move(x)produz um resultado não especificado. Tente! Você não precisa acreditar em mim.
Howard Hinnant
O ponto positivo da @HowardHinnant, swapfunciona desde que x = move(x)saia xem qualquer estado passível de mudança. E os algoritmos std::copy/ std::movesão definidos de modo a produzir um comportamento indefinido em cópias não operacionais (ai; o jovem de 20 anos memmoveacerta o caso trivial, mas std::movenã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.
Quuxplusone
11

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 constreferência sem valor r.

Class &Class::operator=( Class &&rhs ) {
    //...
    return *this;
}

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 = xou y = std::move(y)chama explicitamente , mas o alias, especialmente por meio de várias funções, pode levar a = bou c = std::move(d)tornar-se auto-atribuições. Uma verificação explícita da atribuição automática, ou seja this == &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:

dumb_array& dumb_array::operator=(const dumb_array& other)
{
    if (mSize != other.mSize)
    {
        delete [] mArray;
        mArray = nullptr;  // clear this...
        mSize = 0u;        // ...and this in case the next line throws
        mArray = other.mSize ? new int[other.mSize] : nullptr;
        mSize = other.mSize;
    }
    std::copy(other.mArray, other.mArray + mSize, mArray);
    return *this;
}

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:

class dumb_array
{
    //...
    void swap(dumb_array& other) noexcept
    {
        // Just in case we add UDT members later
        using std::swap;

        // both members are built-in types -> never throw
        swap( this->mArray, other.mArray );
        swap( this->mSize, other.mSize );
    }

    dumb_array& operator=(dumb_array&& other) noexcept
    {
        this->swap( other );
        return *this;
    }
    //...
};

void  swap( dumb_array &l, dumb_array &r ) noexcept  { l.swap( r ); }

Um tipo de troca que precisa de personalização deve ter uma função livre de dois argumentos chamada swapno 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 uma swapfunção de membro pública para corresponder aos contêineres padrão. Se um membro swapnão for fornecido, a função livre swapprovavelmente precisará ser marcada como um amigo do tipo permutável. Se você personalizar as jogadas para usar swap, 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 swapchamada é. 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:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        delete [] this->mArray;  // kill old resources
        this->mArray = other.mArray;
        this->mSize = other.mSize;
        other.mArray = nullptr;  // reset source
        other.mSize = 0u;
        return *this;
    }
    //...
};

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 uma assert(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:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        dumb_array  temp{ std::move(other) };

        this->swap( temp );
        return *this;
    }
    //...
};

Quando othere thissão distintos, otheré esvaziado pela mudança tempe permanece assim. Em seguida, thisperde seus recursos antigos para tempobter os recursos originalmente mantidos por other. Então os antigos recursos de thissão mortos quando o tempfazem.

Quando a auto-atribuição acontece, o esvaziamento do otherque tempesvazia thisbem. Em seguida, o objeto de destino recupera seus recursos quando tempe thistroca. A morte de tempreivindicações um objeto vazio, que deve ser praticamente um não-op. O objeto this/ othermanté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.

CTMacUser
fonte
Você precisa verificar se alguma memória foi alocada antes de chamar deleteseu segundo bloco de código?
user3728501
3
Seu segundo exemplo de código, o operador de atribuição de cópia sem verificação de atribuição automática, está errado. std::copycausa comportamento indefinido se os intervalos de origem e destino se sobrepõem (incluindo o caso quando coincidem). Veja C ++ 14 [alg.copy] / 3.
MM
6

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 implementar operator=, 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):

Expressão Tipo de retorno Valor de retorno Pós-condição
t = rv T & tt é equivalente ao valor de rv antes da atribuição

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 exemplo this != &other), faça-o, caso contrário você não poderá colocar seus objetos std::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 anterior a = std::move(a), ele this == &otherrealmente será válido.

Luc Danton
fonte
Você pode explicar como o a = std::move(a)não funcionamento faria uma classe não funcionar std::vector? Exemplo?
Paul J. Lucas
@ PaulJ.Lucas A chamada std::vector<T>::erasenão é permitida, a menos que Tseja MoveAssignable. (Além do IIRC, alguns requisitos do MoveAssignable foram flexibilizados para o MoveInsertable em vez do C ++ 14.)
Luc Danton
OK, então Tdeve ser MoveAssignable, mas por que erase()dependeria de mover um elemento para si mesmo ?
Paul J. Lucas
@ PaulJ.Lucas Não há resposta satisfatória para essa pergunta. Tudo se resume a 'não quebrar contratos'.
Luc Danton
2

Conforme sua operator=função atual é escrita, desde que você criou o argumento rvalue-reference const, 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 chamar deleteponteiros, etc. em seu thisobjeto, como faria em um operator=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étodo const-lvalue operator=.

Agora, se você definiu operator=para constusar uma referência sem valor, a única maneira pela qual pude ver uma verificação ser necessária era se você passasse o thisobjeto 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:

struct A; //defines operator=(A&& rhs) where it will "steal" the pointers
          //of rhs and set the original pointers of rhs to NULL

A&& operator+(A& rhs, A&& lhs)
{
    //...code

    return std::move(rhs);
}

A&& operator+(A&& rhs, A&&lhs)
{
    //...code

    return std::move(rhs);
}

int main()
{
    A a;

    a = (a + A()) + A(); //calls operator=(A&&) with reference bound to a

    //...rest of code
}

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 thisponteiro.

Jason
fonte
Observe que "a = std :: move (a)" é uma maneira trivial de ter essa situação. Sua resposta é válida.
Vaughn Cato
1
Concordo totalmente que essa é a maneira mais simples, embora eu ache que a maioria das pessoas não fará isso intencionalmente :-) ... Lembre-se de que, se a referência-rvalue for const, você poderá ler apenas nela, portanto, a única necessidade é fazer uma verificação seria se você decidisse operator=(const T&&)executar a mesma reinicialização do thisque faria em um operator=(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).
Jason
1

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:

unique_ptr& operator=(unique_ptr&& x) {
  delete ptr_;
  ptr_ = x.ptr_;
  x.ptr_ = nullptr;
  return *this;
}

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:

src.erase(
  std::partition_copy(src.begin(), src.end(),
                      src.begin(),
                      std::back_inserter(even),
                      [](int num) { return num % 2; }
                      ).first,
  src.end());

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.

  1. Discordo de Howard, que é uma péssima idéia, mas ainda assim - acho que a atribuição de movimentação automática de objetos "removidos" deve funcionar, porque 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).
  2. É assim que a atribuição de unique_ptrs é implementada no libc ++: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} É seguro para a atribuição de movimentação automática.
  3. As Diretrizes Básicas acham que não há problema em mudar a atribuição automaticamente.
Denis Yaroshevskiy
fonte
0

Existe uma situação em que (isso == rhs) eu consigo pensar. Para esta afirmação: Myclass obj; std :: move (obj) = std :: move (obj)

pequeno monstro
fonte
Myclass obj; std :: move (obj) = std :: move (obj);
little_monster