Como std :: move () transfere valores para RValues?

104

Eu simplesmente não entendi totalmente a lógica de std::move().

A princípio, pesquisei no Google, mas parece que só existem documentos sobre como usar std::move(), não como funciona sua estrutura.

Quer dizer, eu sei o que a função de membro do modelo é, mas quando eu olho para a std::move()definição no VS2010, ainda é confuso.

a definição de std :: move () segue abaixo.

template<class _Ty> inline
typename tr1::_Remove_reference<_Ty>::_Type&&
    move(_Ty&& _Arg)
    {   // forward _Arg as movable
        return ((typename tr1::_Remove_reference<_Ty>::_Type&&)_Arg);
    }

O que é estranho primeiro para mim é o parâmetro, (_Ty && _Arg), porque quando eu chamo a função como você vê abaixo,

// main()
Object obj1;
Object obj2 = std::move(obj1);

basicamente é igual a

// std::move()
_Ty&& _Arg = Obj1;

Mas como você já sabe, não é possível vincular diretamente um LValue a uma referência RValue, o que me faz pensar que deveria ser assim.

_Ty&& _Arg = (Object&&)obj1;

No entanto, isso é absurdo porque std :: move () deve funcionar para todos os valores.

Acho que, para entender totalmente como isso funciona, devo dar uma olhada nessas estruturas também.

template<class _Ty>
struct _Remove_reference
{   // remove reference
    typedef _Ty _Type;
};

template<class _Ty>
struct _Remove_reference<_Ty&>
{   // remove reference
    typedef _Ty _Type;
};

template<class _Ty>
struct _Remove_reference<_Ty&&>
{   // remove rvalue reference
    typedef _Ty _Type;
};

Infelizmente, ainda é tão confuso e eu não entendo.

Eu sei que tudo isso é devido à minha falta de habilidades básicas de sintaxe sobre C ++. Gostaria de saber como funcionam bem e quaisquer documentos que eu conseguir na internet serão mais do que bem-vindos. (Se você puder explicar isso, também será incrível).

Dean Seo
fonte
31
@NicolBolas Pelo contrário, a própria pergunta mostra que o OP está se vendendo abaixo do esperado nessa declaração. É precisamente o domínio do OP sobre as habilidades básicas de sintaxe que permite que ele até mesmo coloque a questão.
Kyle Strand
Eu discordo de qualquer maneira - você pode aprender rapidamente ou já ter um bom domínio de ciência da computação ou programação como um todo, mesmo em C ++, e ainda assim ter dificuldades com a sintaxe. É melhor gastar seu tempo aprendendo mecânica, algoritmos, etc., mas confiar em referências para aprimorar a sintaxe conforme necessário, do que ser tecnicamente articulado, mas ter uma compreensão superficial de coisas como copiar / mover / avançar. Como você deve saber "quando e como usar std :: move quando quiser mover, construir algo" antes de saber o que move?
John P
Acho que vim aqui para entender como movefunciona e não como é implementado. Acho esta explicação muito útil: pagefault.blog/2018/03/01/… .
Anton Daneyko

Respostas:

171

Começamos com a função mover (que eu limpei um pouco):

template <typename T>
typename remove_reference<T>::type&& move(T&& arg)
{
  return static_cast<typename remove_reference<T>::type&&>(arg);
}

Vamos começar com a parte mais fácil - isto é, quando a função é chamada com rvalue:

Object a = std::move(Object());
// Object() is temporary, which is prvalue

e nosso movemodelo é instanciado da seguinte maneira:

// move with [T = Object]:
remove_reference<Object>::type&& move(Object&& arg)
{
  return static_cast<remove_reference<Object>::type&&>(arg);
}

Uma vez que remove_referenceconverte T&para Tou T&&para T, e Objectnão é referência, nossa função final é:

Object&& move(Object&& arg)
{
  return static_cast<Object&&>(arg);
}

Agora, você pode se perguntar: nós ainda precisamos do elenco? A resposta é: sim, temos. A razão é simples; a referência de rvalue nomeada é tratada como lvalue (e a conversão implícita de lvalue para a referência de rvalue é proibida por padrão).


Aqui está o que acontece quando chamamos movecom lvalue:

Object a; // a is lvalue
Object b = std::move(a);

e a moveinstanciação correspondente :

// move with [T = Object&]
remove_reference<Object&>::type&& move(Object& && arg)
{
  return static_cast<remove_reference<Object&>::type&&>(arg);
}

Novamente, remove_referenceconverte Object&para Objecte nós obtemos:

Object&& move(Object& && arg)
{
  return static_cast<Object&&>(arg);
}

Agora chegamos à parte complicada: o que Object& &&significa e como pode ser vinculado a lvalue?

Para permitir o encaminhamento perfeito, o padrão C ++ 11 fornece regras especiais para recolhimento de referência, que são as seguintes:

Object &  &  = Object &
Object &  && = Object &
Object && &  = Object &
Object && && = Object &&

Como você pode ver, sob essas regras, Object& &&na verdade Object&, significa que é uma referência lvalue simples que permite vincular lvalues.

A função final é assim:

Object&& move(Object& arg)
{
  return static_cast<Object&&>(arg);
}

que não é diferente da instanciação anterior com rvalue - ambos lançam seu argumento para a referência rvalue e, em seguida, o retornam. A diferença é que a primeira instanciação pode ser usada apenas com rvalues, enquanto a segunda funciona com lvalues.


Para explicar por que precisamos de remove_referenceum pouco mais, vamos tentar esta função

template <typename T>
T&& wanna_be_move(T&& arg)
{
  return static_cast<T&&>(arg);
}

e instancie-o com lvalue.

// wanna_be_move [with T = Object&]
Object& && wanna_be_move(Object& && arg)
{
  return static_cast<Object& &&>(arg);
}

Aplicando as regras de recolhimento de referência mencionadas acima, você pode ver que obtemos uma função que é inutilizável como move(para simplificar, você a chama com lvalue e obtém lvalue de volta). No mínimo, essa função é a função de identidade.

Object& wanna_be_move(Object& arg)
{
  return static_cast<Object&>(arg);
}
Vitus
fonte
3
Boa resposta. Embora eu entenda que para um valor é uma boa ideia avaliar Tcomo Object&, eu não sabia se isso estava realmente feito. Eu esperaria Tavaliar também Objectneste caso, pois pensei que esse era o motivo para a introdução de referências de wrapper e std::ref, ou não era.
Christian Rau
2
Há uma diferença entre template <typename T> void f(T arg)(que é o que está no artigo da Wikipedia) e template <typename T> void f(T& arg). O primeiro resolve para valor (e se você quer passar referência, tem que embrulhá-lo std::ref), enquanto o segundo sempre resolve para referência. Infelizmente, as regras para a dedução do argumento do modelo são bastante complexas, portanto, não posso fornecer um raciocínio preciso por que T&&resovles para Object& &&(mas acontece de fato).
Vitus
1
Mas há alguma razão para que essa abordagem direta não funcione? template <typename T> T&& also_wanna_be_move(T& arg) { return static_cast<T&&>(arg); }
Greggo
1
Isso explica por que remove_reference é necessário, mas ainda não entendi por que a função deve ter T && (em vez de T &). Se eu entendi esta explicação corretamente, não deve importar se você obtém um T && ou T & porque de qualquer forma, remove_reference irá convertê-lo para T, então você adicionará de volta o &&. Então, por que não dizer que você aceita um T & (que semanticamente é o que você está aceitando), em vez de T && e contando com o encaminhamento perfeito para permitir que o chamador passe um T &?
mgiuca
3
@mgiuca: Se você quiser std::moveapenas lançar lvalues ​​em rvalues, então sim, T&tudo bem. Este truque é feito principalmente para flexibilidade: você pode chamar std::movetudo (rvalues ​​incluído) e obter rvalue de volta.
Vitus de
4

_Ty é um parâmetro de modelo e, nesta situação

Object obj1;
Object obj2 = std::move(obj1);

_Ty é o tipo "Objeto &"

é por isso que _Remove_reference é necessário.

Seria mais como

typedef Object& ObjectRef;
Object obj1;
ObjectRef&& obj1_ref = obj1;
Object&& obj2 = (Object&&)obj1_ref;

Se não removêssemos a referência, seria como se estivéssemos fazendo

Object&& obj2 = (ObjectRef&&)obj1_ref;

Mas ObjectRef && se reduz a Object &, que não poderíamos vincular a obj2.

A razão pela qual ele reduz dessa forma é para suportar o encaminhamento perfeito. Veja este artigo .

Vaughn Cato
fonte
Isso não explica tudo sobre por que _Remove_reference_é necessário. Por exemplo, se você tiver Object&como typedef e fizer referência a ele, ainda assim obterá Object&. Por que isso não funciona com &&? Não é uma resposta para isso, e que tem a ver com o encaminhamento perfeito.
Nicol Bolas,
2
Verdade. E a resposta é muito interessante. A & && é reduzido a A &, então se tentássemos usar (ObjectRef &&) obj1_ref, obteríamos Object & neste caso.
Vaughn Cato