Wie überträgt std :: move () Werte in RValues?

102

Ich habe gerade festgestellt, dass ich die Logik von nicht vollständig verstanden habe std::move().

Zuerst habe ich es gegoogelt, aber es scheint nur Dokumente zu geben, wie man es benutzt std::move(), nicht wie seine Struktur funktioniert.

Ich meine, ich weiß, was die Vorlagenelementfunktion ist, aber wenn ich mir die std::move()Definition in VS2010 anschaue, ist sie immer noch verwirrend.

Die Definition von std :: move () ist unten aufgeführt.

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

Was mir zuerst komisch ist, ist der Parameter (_Ty && _Arg), denn wenn ich die Funktion aufrufe, wie Sie unten sehen,

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

es ist im Grunde gleich

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

Aber wie Sie bereits wissen, können Sie einen LValue nicht direkt mit einer RValue-Referenz verknüpfen, weshalb ich denke, dass dies so sein sollte.

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

Dies ist jedoch absurd, da std :: move () für alle Werte funktionieren muss.

Um zu verstehen, wie das funktioniert, sollte ich mir auch diese Strukturen ansehen.

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

Leider ist es immer noch so verwirrend und ich verstehe es nicht.

Ich weiß, dass dies alles auf meinen Mangel an grundlegenden Syntaxkenntnissen in C ++ zurückzuführen ist. Ich würde gerne wissen, wie diese gründlich funktionieren, und alle Dokumente, die ich im Internet erhalten kann, werden mehr als begrüßt. (Wenn Sie dies nur erklären können, wird das auch fantastisch sein).

Dean Seo
quelle
31
@NicolBolas Im Gegenteil, die Frage selbst zeigt, dass OP sich in dieser Aussage selbst unterbietet. Es ist genau das Verständnis von OP für grundlegende Syntaxfähigkeiten, das es ihnen ermöglicht, die Frage sogar zu stellen.
Kyle Strand
Ich bin sowieso anderer Meinung - Sie können schnell lernen oder haben bereits gute Kenntnisse der Informatik oder der Programmierung als Ganzes, auch in C ++, und haben immer noch Probleme mit der Syntax. Es ist besser, Ihre Zeit damit zu verbringen, Mechanik, Algorithmen usw. zu lernen, sich aber auf Referenzen zu verlassen, um die Syntax nach Bedarf aufzufrischen, als technisch zu artikulieren, aber ein flaches Verständnis für Dinge wie Kopieren / Verschieben / Vorwärts zu haben. Woher solltest du wissen, wann und wie du std :: move verwenden sollst, wenn du etwas konstruieren willst, bevor du weißt, was move bewirkt?
John P
Ich glaube, ich bin hierher gekommen, um zu verstehen, wie es movefunktioniert und nicht wie es implementiert wird. Ich finde diese Erklärung wirklich nützlich: pagefault.blog/2018/03/01/… .
Anton Daneyko

Antworten:

170

Wir beginnen mit der Verschiebungsfunktion (die ich ein wenig aufgeräumt habe):

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

Beginnen wir mit dem einfacheren Teil - das heißt, wenn die Funktion mit rvalue aufgerufen wird:

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

und unsere moveVorlage wird wie folgt instanziiert:

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

Da remove_referencekonvertiert T&zu Toder T&&zu Tund Objectkeine Referenz ist, lautet unsere letzte Funktion:

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

Nun fragen Sie sich vielleicht: Brauchen wir überhaupt die Besetzung? Die Antwort lautet: Ja, das tun wir. Der Grund ist einfach; Die benannte rWertreferenz wird als lWert behandelt (und die implizite Konvertierung von lWert in rWertreferenz ist standardmäßig verboten).


moveFolgendes passiert, wenn wir mit lvalue aufrufen:

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

und entsprechende moveInstanziierung:

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

Wieder remove_referencekonvertiert Object&zu Objectund wir bekommen:

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

Jetzt kommen wir zum kniffligen Teil: Was bedeutet das Object& &&überhaupt und wie kann es an den Wert binden?

Um eine perfekte Weiterleitung zu ermöglichen, bietet der C ++ 11-Standard spezielle Regeln für das Reduzieren von Referenzen:

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

Wie Sie sehen können, bedeutet unter diesen Regeln Object& &&tatsächlich Object&, dass es sich um eine einfache L-Wert-Referenz handelt, die das Binden von L-Werten ermöglicht.

Die letzte Funktion ist also:

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

Das ist nicht anders als bei der vorherigen Instanziierung mit rvalue - beide werfen ihr Argument auf die rvalue-Referenz und geben es dann zurück. Der Unterschied besteht darin, dass die erste Instanziierung nur mit r-Werten verwendet werden kann, während die zweite mit l-Werten arbeitet.


Um zu erklären, warum wir remove_referenceetwas mehr brauchen , versuchen wir diese Funktion

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

und instanziiere es mit lvalue.

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

Wenn Sie die oben erwähnten Regeln zum moveReduzieren von Referenzen anwenden, können Sie sehen, dass wir eine Funktion erhalten, die unbrauchbar ist (um es einfach auszudrücken, Sie rufen sie mit lvalue auf, Sie erhalten lvalue zurück). Wenn überhaupt, ist diese Funktion die Identitätsfunktion.

Object& wanna_be_move(Object& arg)
{
  return static_cast<Object&>(arg);
}
Vitus
quelle
3
Gute Antwort. Obwohl ich verstehe, dass es für einen Wert eine gute Idee ist, Tals zu bewerten Object&, wusste ich nicht, dass dies wirklich getan wird. Ich hätte erwartet T, dies auch Objectin diesem Fall zu bewerten , da ich dachte, dies sei der Grund für die Einführung von Wrapper-Referenzen und std::refoder nicht.
Christian Rau
2
Es gibt einen Unterschied zwischen template <typename T> void f(T arg)(was im Wikipedia-Artikel steht) und template <typename T> void f(T& arg). Der erste wird in Wert aufgelöst (und wenn Sie eine Referenz übergeben möchten, müssen Sie ihn einschließen std::ref), während der zweite immer in Referenz aufgelöst wird. Leider sind die Regeln für die Ableitung von Vorlagenargumenten ziemlich komplex, so dass ich keine genauen T&&Gründe dafür liefern kann, warum sie sich ändern Object& &&(aber es passiert tatsächlich).
Vitus
1
Aber gibt es einen Grund, warum dieser direkte Ansatz nicht funktionieren würde? template <typename T> T&& also_wanna_be_move(T& arg) { return static_cast<T&&>(arg); }
Greggo
1
Das erklärt, warum remove_reference notwendig ist, aber ich verstehe immer noch nicht, warum die Funktion T && (anstelle von T &) nehmen muss. Wenn ich diese Erklärung richtig verstehe, sollte es keine Rolle spielen, ob Sie ein T && oder ein T & erhalten, da remove_reference es in beiden Fällen in T umwandelt und Sie das && wieder hinzufügen. Warum also nicht sagen, dass Sie ein T & akzeptieren (was semantisch das ist, was Sie akzeptieren), anstatt T && und sich auf eine perfekte Weiterleitung verlassen, damit der Anrufer ein T & weitergeben kann?
mgiuca
3
@mgiuca: Wenn du std::movenur l-Werte in r-Werte umwandeln willst, dann T&wäre das in Ordnung. Dieser Trick wird hauptsächlich aus std::moveGründen der Flexibilität ausgeführt: Sie können alles aufrufen (einschließlich rWerte) und rWert zurückerhalten.
Vitus
4

_Ty ist in dieser Situation ein Vorlagenparameter

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

_Ty ist der Typ "Object &"

Aus diesem Grund ist die Referenz "Entfernen" erforderlich.

Es wäre eher so

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

Wenn wir die Referenz nicht entfernen würden, wäre es so, wie wir es tun würden

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

ObjectRef && reduziert sich jedoch auf Object &, das wir nicht an obj2 binden konnten.

Der Grund dafür ist die Unterstützung einer perfekten Weiterleitung. Siehe dieses Papier .

Vaughn Cato
quelle
Dies erklärt nicht alles darüber, warum das _Remove_reference_notwendig ist. Wenn Sie Object&beispielsweise ein typedef haben und einen Verweis darauf nehmen, erhalten Sie immer noch Object&. Warum funktioniert das nicht mit &&? Darauf gibt es eine Antwort, die mit perfekter Weiterleitung zu tun hat.
Nicol Bolas
2
Wahr. Und die Antwort ist sehr interessant. A & && wird auf A & reduziert. Wenn wir also versuchen, (ObjectRef &&) obj1_ref zu verwenden, erhalten wir in diesem Fall stattdessen Object &.
Vaughn Cato