std :: move () โอนค่าไปยัง RValues ​​ได้อย่างไร


105

ฉันเพิ่งพบว่าตัวเองไม่เข้าใจตรรกะของstd::move().

ตอนแรกฉันใช้ googled แต่ดูเหมือนว่าจะมีเพียงเอกสารเกี่ยวกับวิธีใช้std::move()ไม่ใช่วิธีการทำงานของโครงสร้าง

ฉันหมายความว่าฉันรู้ว่าฟังก์ชันสมาชิกเทมเพลตคืออะไร แต่เมื่อฉันดูstd::move()คำจำกัดความใน VS2010 ก็ยังสับสน

คำจำกัดความของ std :: move () อยู่ด้านล่าง

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

สิ่งที่แปลกก่อนสำหรับฉันคือพารามิเตอร์ (_Ty && _Arg) เพราะเมื่อฉันเรียกใช้ฟังก์ชันอย่างที่คุณเห็นด้านล่าง

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

โดยพื้นฐานแล้วมันเท่ากับ

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

แต่อย่างที่คุณทราบแล้วคุณไม่สามารถเชื่อมโยง LValue กับข้อมูลอ้างอิง RValue ได้โดยตรงซึ่งทำให้ฉันคิดว่ามันควรจะเป็นแบบนี้

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

อย่างไรก็ตามนี่เป็นเรื่องที่ไร้สาระเนื่องจาก std :: move () ต้องใช้ได้กับค่าทั้งหมด

ดังนั้นฉันเดาว่าจะเข้าใจอย่างถ่องแท้ว่ามันทำงานอย่างไรฉันควรดูโครงสร้างเหล่านี้ด้วย

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

น่าเสียดายที่มันยังสับสนและฉันไม่เข้าใจ

ฉันรู้ว่าทั้งหมดนี้เป็นเพราะฉันไม่มีทักษะพื้นฐานทางไวยากรณ์เกี่ยวกับ C ++ ฉันต้องการทราบวิธีการทำงานอย่างละเอียดและเอกสารใด ๆ ที่ฉันสามารถหาได้ทางอินเทอร์เน็ตจะยินดีเป็นอย่างยิ่ง (ถ้าอธิบายได้แค่นี้ก็น่าจะยอดเยี่ยมเช่นกัน)


32
@NicolBolas ในทางตรงกันข้ามคำถามนั้นแสดงให้เห็นว่า OP กำลังบอกตัวเองในข้อความนั้น เป็นความเข้าใจทักษะไวยากรณ์พื้นฐานของOP อย่างแม่นยำซึ่งช่วยให้พวกเขาตั้งคำถามได้
Kyle Strand

ฉันไม่เห็นด้วยอยู่แล้ว - คุณสามารถเรียนรู้ได้อย่างรวดเร็วหรือมีความเข้าใจวิทยาศาสตร์คอมพิวเตอร์หรือการเขียนโปรแกรมโดยรวมเป็นอย่างดีแม้ใน C ++ และยังคงต่อสู้กับไวยากรณ์ ควรใช้เวลาในการเรียนรู้กลศาสตร์อัลกอริทึมและอื่น ๆ แต่ควรใช้การอ้างอิงเพื่อแปรงไวยากรณ์ตามความจำเป็นมากกว่าที่จะเป็นข้อสรุปทางเทคนิค แต่ต้องมีความเข้าใจตื้น ๆ ในสิ่งต่างๆเช่น copy / move / forward คุณควรรู้ได้อย่างไรว่า "ใช้ std :: move เมื่อคุณต้องการย้ายสร้างอะไร" ก่อนที่คุณจะรู้ว่าการย้ายทำอะไร?
John P

ฉันคิดว่าฉันมาที่นี่เพื่อทำความเข้าใจวิธีการmoveทำงานมากกว่าวิธีการใช้งาน ฉันพบว่าคำอธิบายนี้มีประโยชน์จริงๆ: pagefault.blog/2018/03/01/… .
Anton Daneyko

คำตอบ:


172

เราเริ่มต้นด้วยฟังก์ชั่นการย้าย (ซึ่งฉันทำความสะอาดเล็กน้อย):

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

เริ่มจากส่วนที่ง่ายกว่านั่นคือเมื่อฟังก์ชันถูกเรียกด้วย rvalue:

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

และmoveเทมเพลตของเราได้รับการสร้างอินสแตนซ์ดังนี้:

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

เนื่องจากremove_referenceแปลงT&เป็นTหรือT&&เป็นTและObjectไม่ได้อ้างอิงฟังก์ชันสุดท้ายของเราคือ:

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

ตอนนี้คุณอาจสงสัยว่าเราต้องการนักแสดงหรือไม่? คำตอบคือใช่เราทำ เหตุผลนั้นง่ายมาก การอ้างอิงที่มีชื่อว่า rvalue ถือว่าเป็น lvalue (และการแปลงโดยนัยจาก lvalue เป็น rvalue เป็นสิ่งต้องห้ามตามมาตรฐาน)


นี่คือสิ่งที่เกิดขึ้นเมื่อเราโทรหาmovelvalue:

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

และการmoveสร้างอินสแตนซ์ที่เกี่ยวข้อง:

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

อีกครั้งremove_referenceแปลงObject&เป็นObjectและเราได้รับ:

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

ตอนนี้เรามาถึงส่วนที่ยุ่งยาก: ความObject& &&หมายคืออะไรและจะผูกกับ lvalue ได้อย่างไร?

เพื่อให้การส่งต่อสมบูรณ์แบบมาตรฐาน C ++ 11 มีกฎพิเศษสำหรับการยุบอ้างอิงซึ่งมีดังต่อไปนี้:

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

อย่างที่คุณเห็นภายใต้กฎเหล่านี้Object& &&หมายถึงจริงObject&ซึ่งเป็นการอ้างอิง lvalue ธรรมดาที่อนุญาตให้มีค่าผูกมัด

ฟังก์ชันสุดท้ายคือ:

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

ซึ่งไม่ต่างจากการสร้างอินสแตนซ์ก่อนหน้าด้วย rvalue - ทั้งคู่โยนอาร์กิวเมนต์เพื่ออ้างอิง rvalue แล้วส่งกลับ ความแตกต่างคือการสร้างอินสแตนซ์แรกสามารถใช้ได้กับ rvalues ​​เท่านั้นในขณะที่การสร้างอินสแตนซ์ที่สองทำงานกับ lvalues


เพื่ออธิบายว่าทำไมเราถึงต้องการremove_referenceมากกว่านี้มาลองใช้ฟังก์ชันนี้กัน

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

และสร้างอินสแตนซ์ด้วย lvalue

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

การใช้กฎการยุบการอ้างอิงที่กล่าวถึงข้างต้นคุณจะเห็นว่าเราได้รับฟังก์ชันที่ใช้ไม่ได้move(พูดง่ายๆคือคุณเรียกมันด้วย lvalue คุณจะได้รับ lvalue กลับมา) ถ้ามีอะไรฟังก์ชันนี้คือฟังก์ชันเอกลักษณ์

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

3
คำตอบที่ดี แม้ว่าฉันเข้าใจว่าสำหรับ lvalue เป็นความคิดที่ดีที่จะประเมินTเป็นObject&แต่ฉันไม่รู้ว่าสิ่งนี้ทำได้จริง ฉันคาดว่าTจะประเมินObjectด้วยเช่นกันในกรณีนี้เนื่องจากฉันคิดว่านี่เป็นเหตุผลในการแนะนำการอ้างอิง Wrapper และstd::refหรือไม่ก็ตาม
Christian Rau

2
มีความแตกต่างระหว่างtemplate <typename T> void f(T arg)(ซึ่งเป็นสิ่งที่อยู่ในบทความวิกิพีเดีย) template <typename T> void f(T& arg)และ อันแรกเปลี่ยนเป็นค่า (และถ้าคุณต้องการส่งต่อการอ้างอิงคุณต้องรวมเข้าด้วยกันstd::ref) ในขณะที่อันที่สองจะแก้ไขเป็นการอ้างอิงเสมอ น่าเศร้าที่กฎสำหรับการหักอาร์กิวเมนต์แม่แบบค่อนข้างซับซ้อนดังนั้นฉันจึงไม่สามารถให้เหตุผลที่ชัดเจนได้ว่าทำไมจึงT&&เปลี่ยนเป็นObject& &&(แต่เกิดขึ้นจริง)
Vitus

1
แต่มีเหตุผลใดที่แนวทางโดยตรงนี้จะไม่ได้ผล? template <typename T> T&& also_wanna_be_move(T& arg) { return static_cast<T&&>(arg); }
greggo

1
นั่นอธิบายได้ว่าทำไมต้อง remove_reference แต่ฉันยังไม่เข้าใจว่าทำไมฟังก์ชันถึงต้องใช้ T && (แทนที่จะเป็น T &) ถ้าฉันเข้าใจคำอธิบายนี้ถูกต้องไม่สำคัญว่าคุณจะได้รับ T && หรือ T & เพราะไม่ว่าจะด้วยวิธีใด remove_reference จะส่งเป็น T จากนั้นคุณจะเพิ่ม && กลับเข้าไป แล้วทำไมไม่บอกว่าคุณยอมรับ T & (ซึ่งตามความหมายคือสิ่งที่คุณยอมรับ) แทนที่จะเป็น T && และอาศัยการส่งต่อที่สมบูรณ์แบบเพื่อให้ผู้โทรผ่าน T &?
mgiuca

3
@mgiuca: ถ้าคุณต้องการstd::moveเฉพาะ lvalues ​​เป็น rvalues ​​ใช่T&ก็จะโอเค เคล็ดลับนี้ทำเพื่อความยืดหยุ่นเป็นส่วนใหญ่: คุณสามารถเรียกstd::moveทุกอย่าง (รวมค่า rvalues) และรับค่า rvalue กลับมา
Vitus

4

_Ty เป็นพารามิเตอร์เทมเพลตและในสถานการณ์นี้

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

_Ty คือประเภท "Object &"

ซึ่งเป็นเหตุผลว่าทำไม _Remove_reference จึงจำเป็น

มันจะชอบมากขึ้น

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

หากเราไม่ได้ลบการอ้างอิงก็จะเหมือนกับว่าเรากำลังทำอยู่

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

แต่ ObjectRef && ลดเป็น Object & ซึ่งเราไม่สามารถผูกกับ obj2 ได้

เหตุผลที่ลดวิธีนี้คือการสนับสนุนการส่งต่อที่สมบูรณ์แบบ ดูกระดาษนี้


นี่ไม่ได้อธิบายทุกอย่างเกี่ยวกับสาเหตุที่_Remove_reference_จำเป็น ตัวอย่างเช่นถ้าคุณมีObject&เป็น typedef Object&และคุณใช้การอ้างอิงถึงว่าคุณยังคงได้รับ เหตุใดจึงใช้ไม่ได้กับ && มีเป็นคำตอบที่และมันจะทำอย่างไรกับการส่งต่อที่สมบูรณ์แบบ
Nicol Bolas

2
จริง. และคำตอบนั้นน่าสนใจมาก A & && จะลดลงเป็น A & ดังนั้นหากเราพยายามใช้ (ObjectRef &&) obj1_ref เราจะได้ Object & แทนในกรณีนี้
Vaughn Cato
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.