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

128

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

Howard Hinnant
fonte
Para piorar a situação, os três-argumento std::moverealmente se move ..
Cubbi
E não se esqueça sobre o C ++ 98 / 11/03 std::char_traits::move:-)
Howard Hinnant
30
Meu outro favorito é o std::remove()que não remove os elementos: você ainda precisa ligar erase()para remover esses elementos do contêiner. Então movenão se move, removenão remove. Eu teria escolhido o nome mark_movable()para move.
Ali
4
@ Ali eu também acharia mark_movable()confuso. Isso sugere que há um efeito colateral duradouro, onde na verdade não existe.
finnw

Respostas:

178

É 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 moveremonta à 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 swapdeve 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 swapfosse 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:

Taxonomia

(imagem descaradamente roubada de dirkgently )

E então hoje, se quiséssemos swapdizer 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::movenã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_xvaluee 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 movefaz 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::movenã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++filtela 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::movefez nada além de converter jem um rvalue e, em seguida, esse rvalue Xse 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++filtrevela 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.

Howard Hinnant
fonte
7
Aqui está uma fonte de pontos para esse gráfico: eu o recriei digraph D { glvalue -> { lvalue; xvalue } rvalue -> { xvalue; prvalue } expression -> { glvalue; rvalue } }para o bem público :) Faça o download aqui como SVG
veja
7
Isso ainda está aberto ao ciclismo? Eu sugiro allow_move;)
dyp
2
@dyp Meu favorito ainda é movable.
Daniel Frey
6
Scott Meyers sugeriu renomear std::movepara rvalue_cast: youtube.com/…
nairware
6
Como rvalue agora se refere a ambos os valores, x e x, rvalue_casté ambíguo em seu significado: que tipo de rvalor ele retorna? xvalue_castseria um nome consistente aqui. Infelizmente, a maioria das pessoas, neste momento, também não entenderia o que está fazendo. Em mais alguns anos, espero que minha declaração se torne falsa.
Howard Hinnant 27/01
20

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

move (x) significa "você pode tratar x como um rvalue". Talvez fosse melhor se move () tivesse sido chamado rval (), mas agora move () já é usado há anos.

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

podkova
fonte
2
Para plagiar o comentário de @ HowardHinnant de outra resposta: A resposta Stroustrup é imprecisa, pois agora existem dois tipos de rvalues ​​- prvalues ​​e xvalues, e std :: move é realmente um elenco de xvalue.
einpoklum