Por que o `std :: move` é chamado` std :: move`?

A function C ++ 11 std::move(x) realmente não move nada. É apenas um casting para o valor r. Por que isso foi feito? Isso não é enganoso?

É correto que std::move(x) seja apenas um cast para rvalue – mais especificamente para um xvalue , em oposição a um prvalue . E também é verdade que ter um casting chamado move às vezes confunde as pessoas. No entanto, a intenção dessa nomenclatura não é confundir, mas sim tornar seu código mais legível.

A história da move remonta à proposta de mudança original em 2002 . Este artigo primeiro introduz a referência rvalue e, em seguida, mostra como escrever um std::swap mais eficiente:

 template  void swap(T& a, T& b) { T tmp(static_cast(a)); a = static_cast(b); b = static_cast(tmp); } 

É preciso lembrar que, neste ponto da história, a única coisa que ” && ” poderia significar era lógica e lógica . Ninguém estava familiarizado com referências de valor, nem sobre as implicações de converter um lvalue para um rvalue (embora não fazer uma cópia como static_cast(t) faria). Então, os leitores deste código pensariam naturalmente:

Eu sei como a swap deve funcionar (copiar para temporária e depois trocar os valores), mas qual é o propósito daqueles castings feios ?!

Note também que swap é realmente apenas um swap para todos os tipos de algoritmos de modificação de permutação. Essa discussão é muito maior que a swap .

Em seguida, a proposta introduz o açúcar de syntax, que substitui o static_cast por algo mais legível, que não indica precisamente o quê , mas o motivo :

 template  void swap(T& a, T& b) { T tmp(move(a)); a = move(b); b = move(tmp); } 

Ou seja, move é apenas syntax sugar para static_cast , e agora o código é bastante sugestivo sobre o porquê desses casts estarem lá: para habilitar a semântica de movimento!

É preciso entender que, no contexto da história, poucas pessoas neste momento realmente entenderam a conexão íntima entre valores e semântica de movimento (embora o trabalho tente explicar isso também):

Mover a semântica entrará automaticamente em jogo quando receber argumentos rvalue. Isso é perfeitamente seguro porque a movimentação de resources de um rvalue não pode ser percebida pelo resto do programa ( ninguém mais tem uma referência ao rvalue para detectar uma diferença ).

Se, no momento, a swap fosse apresentada assim:

 template  void swap(T& a, T& b) { T tmp(cast_to_rvalue(a)); a = cast_to_rvalue(b); b = cast_to_rvalue(tmp); } 

Então as pessoas teriam olhado para aquilo e dito:

Mas por que você está lançando para valorizar?


O ponto principal:

Como foi, usando o move , ninguém nunca perguntou:

Mas por que você está se movendo?


Com o passar dos anos e a proposta foi refinada, as noções de lvalue e rvalue foram refinadas para as categorias de valor que temos hoje:

Taxonomia

(imagem descaradamente roubada de dirkgently )

E então, hoje, se quisermos swap para dizer exatamente o que está fazendo, em vez de por quê , deve parecer mais com:

 template  void swap(T& a, T& b) { T tmp(set_value_category_to_xvalue(a)); a = set_value_category_to_xvalue(b); b = set_value_category_to_xvalue(tmp); } 

E a questão que todos deveriam estar se perguntando é se o código acima é mais ou menos legível que:

 template  void swap(T& a, T& b) { T tmp(move(a)); a = move(b); b = move(tmp); } 

Ou até o original:

 template  void swap(T& a, T& b) { T tmp(static_cast(a)); a = static_cast(b); b = static_cast(tmp); } 

Em qualquer caso, o programador C ++ deve saber que sob o capô do move , nada mais está acontecendo do que um casting. E o novato programador C ++, pelo menos com o move , será informado de que a intenção é passar dos rhs, ao invés de copiar dos rhs, mesmo que eles não entendam exatamente como isso é feito.

Além disso, se um programador desejar essa funcionalidade com outro nome, std::move não possui monopólio sobre essa funcionalidade e não há nenhuma mágica de linguagem não portátil envolvida em sua implementação. Por exemplo, se alguém quisesse codificar set_value_category_to_xvalue , e usar isso em vez disso, é trivial fazer isso:

 template  inline constexpr typename std::remove_reference::type&& set_value_category_to_xvalue(T&& t) noexcept { return static_cast::type&&>(t); } 

Em C ++ 14, fica ainda mais conciso:

 template  inline constexpr auto&& set_value_category_to_xvalue(T&& t) noexcept { return static_cast&&>(t); } 

Então, se você é tão inclinado, decore seu static_cast que achar melhor, e talvez acabe desenvolvendo uma nova prática recomendada (o C ++ está em constante evolução).

Então, o que move fazer em termos de código object gerado?

Considere este test :

 void test(int& i, int& j) { i = j; } 

Compilado com clang++ -std=c++14 test.cpp -O3 -S , isso produz este código de object:

 __Z4testRiS_: ## @_Z4testRiS_ .cfi_startproc ## BB#0: pushq %rbp Ltmp0: .cfi_def_cfa_offset 16 Ltmp1: .cfi_offset %rbp, -16 movq %rsp, %rbp Ltmp2: .cfi_def_cfa_register %rbp movl (%rsi), %eax movl %eax, (%rdi) popq %rbp retq .cfi_endproc 

Agora, se o teste for alterado para:

 void test(int& i, int& j) { i = std::move(j); } 

Não há absolutamente nenhuma alteração no código do object. Pode-se generalizar este resultado para: Para objects móveis trivialmente , o std::move não tem impacto.

Agora vamos ver este exemplo:

 struct X { X& operator=(const X&); }; void test(X& i, X& j) { i = j; } 

Isso gera:

 __Z4testR1XS0_: ## @_Z4testR1XS0_ .cfi_startproc ## BB#0: pushq %rbp Ltmp0: .cfi_def_cfa_offset 16 Ltmp1: .cfi_offset %rbp, -16 movq %rsp, %rbp Ltmp2: .cfi_def_cfa_register %rbp popq %rbp jmp __ZN1XaSERKS_ ## TAILCALL .cfi_endproc 

Se você executar __ZN1XaSERKS_ através de c++filt ele produzirá: X::operator=(X const&) . Nenhuma surpresa aqui. Agora, se o teste for alterado para:

 void test(X& i, X& j) { i = std::move(j); } 

Então, ainda não há alteração alguma no código do object gerado. std::move não fez nada além de converter j em um rvalue, e então esse rvalue X se liga ao operador de atribuição de cópia de X

Agora vamos adicionar um operador de atribuição de movimento ao X :

 struct X { X& operator=(const X&); X& operator=(X&&); }; 

Agora o código do object muda:

 __Z4testR1XS0_: ## @_Z4testR1XS0_ .cfi_startproc ## BB#0: pushq %rbp Ltmp0: .cfi_def_cfa_offset 16 Ltmp1: .cfi_offset %rbp, -16 movq %rsp, %rbp Ltmp2: .cfi_def_cfa_register %rbp popq %rbp jmp __ZN1XaSEOS_ ## TAILCALL .cfi_endproc 

Executar __ZN1XaSEOS_ através de c++filt revela que X::operator=(X&&) está sendo chamado em vez de X::operator=(X const&) .

E isso é tudo que existe para std::move ! Ele desaparece completamente no tempo de execução. Seu único impacto é em tempo de compilation, onde pode alterar a sobrecarga que é chamada.

Deixe-me deixar aqui uma citação do FAQ do C ++ 11 escrito por B. Stroustrup, que é uma resposta direta à pergunta do OP:

move (x) significa “você pode tratar x como um rvalue”. Talvez teria sido melhor se move () tivesse sido chamado rval (), mas agora move () tem sido usado há anos.

A propósito, gostei muito do FAQ – vale a pena ler.