É correto que std::move(x)
seja apenas uma conversão para rvalue - mais especificamente para um xvalue , em oposição a um prvalue . E também é verdade que ter um elenco chamado move
às vezes confunde as pessoas. No entanto, a intenção dessa nomeação não é confundir, mas sim tornar seu código mais legível.
A história move
remonta à proposta original de mudança em 2002 . Este artigo apresenta primeiro a referência rvalue e, em seguida, mostra como escrever uma descrição mais eficiente std::swap
:
template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}
É preciso lembrar que, neste ponto da história, a única coisa que " &&
" poderia significar era lógica e . Ninguém estava familiarizado com as referências rvalue, nem com as implicações de converter um lvalue em um rvalue (sem fazer uma cópia como static_cast<T>(t)
faria). Portanto, os leitores desse código pensariam naturalmente:
Eu sei como swap
deve funcionar (copie para temporário e depois troque os valores), mas qual é o propósito desses lançamentos feios ?!
Observe também que swap
é realmente apenas um substituto para todos os tipos de algoritmos de modificação de permutação. Essa discussão é muito , muito maior que swap
.
Em seguida, a proposta introduz o açúcar de sintaxe, que substitui o static_cast<T&&>
por algo mais legível que transmite não o que é preciso , mas o porquê :
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
Ou seja, move
é apenas o açúcar da sintaxe static_cast<T&&>
, e agora o código é bastante sugestivo sobre o porquê desses elencos existem: ativar a semântica de movimento!
É preciso entender que, no contexto da história, poucas pessoas realmente entenderam a conexão íntima entre rvalues e semântica de movimento (embora o artigo tente explicar isso também):
A semântica de movimento entrará em jogo automaticamente quando receberem argumentos de rvalue. Isso é perfeitamente seguro porque a movimentação de recursos de um rvalue não pode ser percebida pelo restante do programa ( ninguém mais tem uma referência ao rvalue para detectar uma diferença ).
Se na época swap
fosse apresentado assim:
template <class T>
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 isso e dito:
Mas por que você está lançando um valor para o valor?
O ponto principal:
Como era move
, ninguém nunca perguntou:
Mas por que você está se mudando?
Com o passar dos anos e a proposta foi refinada, as noções de lvalue e rvalue foram refinadas nas categorias de valor que temos hoje:
(imagem descaradamente roubada de dirkgently )
E então hoje, se quiséssemos swap
dizer com precisão o que está fazendo, em vez de por que , deveria parecer mais:
template <class T>
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 pergunta que todos deveriam se perguntar é se o código acima é mais ou menos legível do que:
template <class T>
void
swap(T& a, T& b)
{
T tmp(move(a));
a = move(b);
b = move(tmp);
}
Ou até o original:
template <class T>
void
swap(T& a, T& b)
{
T tmp(static_cast<T&&>(a));
a = static_cast<T&&>(b);
b = static_cast<T&&>(tmp);
}
De qualquer forma, o programador C ++ deve saber que, sob o capô de move
, nada mais está acontecendo do que um elenco. E o programador iniciante em C ++, pelo menos com move
, será informado de que a intenção é sair do rhs, em vez de copiar do rhs, mesmo que eles não entendam exatamente como isso é realizado.
Além disso, se um programador desejar essa funcionalidade com outro nome, ele std::move
não possuirá monopólio dessa funcionalidade e não haverá mágica de linguagem não portátil envolvida em sua implementação. Por exemplo, se alguém quiser codificar set_value_category_to_xvalue
e usar isso, é trivial fazer isso:
template <class T>
inline
constexpr
typename std::remove_reference<T>::type&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
No C ++ 14, fica ainda mais conciso:
template <class T>
inline
constexpr
auto&&
set_value_category_to_xvalue(T&& t) noexcept
{
return static_cast<std::remove_reference_t<T>&&>(t);
}
Portanto, se você é tão inclinado, decore o static_cast<T&&>
que achar melhor e, talvez, acabe desenvolvendo uma nova melhor prática (o C ++ está em constante evolução).
Então, o que move
faz em termos de código de objeto gerado?
Considere isto 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 objeto:
__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 objeto. Pode-se generalizar esse resultado para: Para objetos trivialmente móveis , 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 c++filt
ela produz: X::operator=(X const&)
. Nenhuma surpresa aqui. Agora, se o teste for alterado para:
void
test(X& i, X& j)
{
i = std::move(j);
}
Ainda não há nenhuma alteração no código do objeto gerado. std::move
fez nada além de converter j
em um rvalue e, em seguida, 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 a X
:
struct X
{
X& operator=(const X&);
X& operator=(X&&);
};
Agora o código objeto faz a mudança:
__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
Correndo __ZN1XaSEOS_
através c++filt
revela que X::operator=(X&&)
está sendo chamado em vez de X::operator=(X const&)
.
E é só isso std::move
! Desaparece completamente em tempo de execução. Seu único impacto é no tempo de compilação, onde pode alterar a sobrecarga chamada.
std::move
realmente se move ..