W jaki sposób std :: move () przenosi wartości do RValues?

105

Po prostu stwierdziłem, że nie w pełni rozumiem logikę std::move().

Na początku wyszukałem go w Google, ale wydaje się, że są tylko dokumenty o tym, jak używać std::move(), a nie o tym , jak działa jego struktura.

Chodzi mi o to, że wiem, jaka jest funkcja elementu członkowskiego szablonu, ale kiedy patrzę na std::move()definicję w VS2010, nadal jest myląca.

definicja std :: move () znajduje się poniżej.

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);
    }

Przede wszystkim dziwny jest dla mnie parametr (_Ty && _Arg), ponieważ kiedy wywołuję funkcję, jak widać poniżej,

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

w zasadzie równa się

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

Ale jak już wiesz, nie można bezpośrednio powiązać LValue z odniesieniem RValue, co sprawia, że ​​myślę, że powinno tak być.

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

Jest to jednak absurdalne, ponieważ std :: move () musi działać dla wszystkich wartości.

Więc myślę, że aby w pełni zrozumieć, jak to działa, powinienem również przyjrzeć się tym strukturom.

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;
};

Niestety nadal jest to zagmatwane i nie rozumiem.

Wiem, że to wszystko z powodu mojego braku podstawowych umiejętności składniowych w C ++. Chciałbym wiedzieć, jak dokładnie działają, a wszelkie dokumenty, które mogę znaleźć w Internecie, będą mile widziane. (Jeśli możesz to tylko wyjaśnić, to też będzie niesamowite).

Dean Seo
źródło
32
@NicolBolas Wręcz przeciwnie, samo pytanie pokazuje, że OP zaniża ceny w tym oświadczeniu. To właśnie OP zrozumienie podstawowych umiejętności składni, które pozwala im nawet postawić pytanie.
Kyle Strand
I tak się nie zgadzam - możesz się szybko uczyć lub już dobrze rozumieć informatykę lub programowanie jako całość, nawet w C ++, i nadal masz problemy ze składnią. Lepiej jest poświęcić czas na naukę mechaniki, algorytmów itp., Ale polegać na odniesieniach, aby w razie potrzeby odświeżyć składnię, niż na wyrażaniu technicznym, ale płytko rozumiejąc rzeczy takie jak kopiuj / przenieś / naprzód. Skąd masz wiedzieć, „kiedy i jak używać std :: move, kiedy chcesz przenieść coś konstruuj”, zanim wiesz, co robi move?
John P
Myślę, że przyjechałem tutaj, aby zrozumieć, jak movedziała, a nie jak jest wdrażane. Uważam, że to wyjaśnienie jest naprawdę przydatne: pagefault.blog/2018/03/01/… .
Anton Daneyko

Odpowiedzi:

172

Zaczynamy od funkcji move (którą trochę wyczyściłem):

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

Zacznijmy od łatwiejszej części - czyli gdy funkcja jest wywoływana z rvalue:

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

a nasz moveszablon zostanie utworzony w następujący sposób:

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

Ponieważ remove_referencekonwertuje T&do Tlub T&&do Ti Objectnie jest odniesieniem, nasza ostatnia funkcja to:

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

Teraz możesz się zastanawiać: czy w ogóle potrzebujemy obsady? Odpowiedź brzmi: tak, robimy. Powód jest prosty; nazwane odwołanie do wartości r jest traktowane jako lwartość (a niejawna konwersja z lwartości na odwołanie do r wartości jest standardowo zabroniona).


Oto, co się dzieje, gdy wywołujemy movez lvalue:

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

i odpowiednia moveinstancja:

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

Ponownie remove_referencekonwertujemy Object&na Objecti otrzymujemy:

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

Teraz przechodzimy do trudnej części: co to w Object& &&ogóle oznacza i jak może wiązać się z lwartością?

Aby umożliwić idealne przekazywanie, standard C ++ 11 zapewnia specjalne reguły zwijania referencji, które są następujące:

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

Jak widać, zgodnie z tymi regułami Object& &&faktycznie oznacza to Object&, że jest to zwykłe odniesienie do lwartości, które umożliwia wiązanie l-wartości.

Ostateczna funkcja to:

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

co nie różni się od poprzedniego wystąpienia z rvalue - oba rzutują swój argument na odwołanie do rvalue, a następnie zwracają go. Różnica polega na tym, że pierwsza instancja może być używana tylko z rvalues, podczas gdy druga działa z lvalues.


Aby wyjaśnić, dlaczego potrzebujemy remove_referencetrochę więcej, wypróbujmy tę funkcję

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

i utwórz instancję z lvalue.

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

Stosując wspomniane powyżej reguły zwijania referencji, możesz zobaczyć, że otrzymujemy funkcję, która jest bezużyteczna jako move(mówiąc prościej, wywołujesz ją z lvalue, otrzymujesz lvalue z powrotem). Jeśli już, ta funkcja jest funkcją tożsamości.

Object& wanna_be_move(Object& arg)
{
  return static_cast<Object&>(arg);
}
Vitus
źródło
3
Niezła odpowiedź. Chociaż rozumiem, że w przypadku lwartości dobrym pomysłem jest ocenianie Tjako Object&, nie wiedziałem, że tak się dzieje. Spodziewałbym Tsię również Objectw tym przypadku, ponieważ myślałem, że to jest powód wprowadzenia referencji opakowujących i std::ref, czy nie było.
Christian Rau,
2
Istnieje różnica między template <typename T> void f(T arg)(co jest w artykule w Wikipedii) i template <typename T> void f(T& arg). Pierwsza z nich oznacza wartość (i jeśli chcesz przekazać referencję, musisz ją zawinąć std::ref), podczas gdy druga zawsze jest rozpoznawana jako referencja. Niestety, zasady dedukcji argumentów z szablonu są dość złożone, więc nie mogę podać dokładnego uzasadnienia, dlaczego T&&tak się Object& &&dzieje (ale tak się dzieje).
Vitus,
1
Ale czy jest jakiś powód, dla którego to bezpośrednie podejście nie zadziałałoby? template <typename T> T&& also_wanna_be_move(T& arg) { return static_cast<T&&>(arg); }
greggo,
1
To wyjaśnia, dlaczego remove_reference jest konieczne, ale nadal nie rozumiem, dlaczego funkcja musi pobierać T && (zamiast T &). Jeśli dobrze zrozumiem to wyjaśnienie, nie powinno mieć znaczenia, czy otrzymasz T &&, czy T &, ponieważ w obu przypadkach remove_reference przerzuci je na T, a następnie dodasz z powrotem &&. Dlaczego więc nie powiedzieć, że akceptujesz T & (który semantycznie jest tym, co akceptujesz), zamiast T && i polegać na idealnym przekazywaniu, aby umożliwić dzwoniącemu przekazanie T &?
mgiuca
3
@mgiuca: Jeśli chcesz std::movetylko rzucać lvalues ​​na rvalues, to tak, T&byłoby w porządku. Ta sztuczka jest wykonywana głównie dla elastyczności: możesz wywołać std::movewszystko (włączając wartości r) i odzyskać rwartość.
Vitus
4

_Ty to parametr szablonu iw tej sytuacji

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

_Ty jest typu „Object &”

dlatego konieczne jest _Remove_reference.

Byłoby bardziej jak

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

Gdybyśmy nie usunęli odniesienia, wyglądałoby to tak, jak robiliśmy

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

Ale ObjectRef && redukuje się do Object &, którego nie mogliśmy powiązać z obj2.

Powodem, dla którego zmniejsza się w ten sposób, jest wspieranie doskonałego przekazywania. Zobacz ten artykuł .

Vaughn Cato
źródło
To nie wyjaśnia wszystkiego, dlaczego _Remove_reference_jest to konieczne. Na przykład, jeśli masz Object&typedef i weźmiesz do niego odniesienie, nadal otrzymujesz Object&. Dlaczego to nie działa z &&? Tam to odpowiedź i ma to związek z perfekcyjnym przekazywaniem.
Nicol Bolas,
2
Prawdziwe. A odpowiedź jest bardzo interesująca. A & && jest redukowane do A &, więc gdybyśmy spróbowali użyć (ObjectRef &&) obj1_ref, w tym przypadku otrzymalibyśmy Object &.
Vaughn Cato