เหตุใด "std :: move" จึงมีชื่อว่า "std :: move"


129

ฟังก์ชัน C ++ 11 std::move(x)ไม่ได้เคลื่อนไหวอะไรเลย มันเป็นเพียงแค่การเพิ่มค่า r ทำไมถึงทำเช่นนี้? นี่ไม่ทำให้เข้าใจผิดเหรอ?


เพื่อให้เรื่องเลวร้ายลงการโต้เถียงทั้งสามstd::moveได้เคลื่อนไหวอย่างแท้จริง ..
Cubbi

และอย่าลืม C ++ std::char_traits::move
98/03/11

30
รายการโปรดอีกอย่างของฉันคือstd::remove()ไม่ได้ลบองค์ประกอบ: คุณยังต้องเรียกร้องerase()ให้ลบองค์ประกอบเหล่านั้นออกจากคอนเทนเนอร์จริงๆ ดังนั้นmoveไม่ย้ายremoveไม่ลบ ฉันจะได้เลือกชื่อสำหรับmark_movable() move
Ali

4
@ อาลีฉันจะรู้สึกmark_movable()สับสนด้วย แสดงให้เห็นว่ามีผลข้างเคียงที่ยาวนานซึ่งในความเป็นจริงไม่มีเลย
finnw

คำตอบ:


179

มันเป็นความถูกต้องที่std::move(x)เป็นเพียงการโยนไป rvalue - การมากขึ้นโดยเฉพาะกับผู้Xvalueเมื่อเทียบกับ prvalue และก็เป็นความจริงเช่นกันที่moveบางครั้งการมีนักแสดงที่มีชื่อก็ทำให้ผู้คนสับสน อย่างไรก็ตามเจตนาของการตั้งชื่อนี้ไม่ได้ทำให้สับสน แต่เพื่อให้โค้ดของคุณอ่านง่ายขึ้น

ประวัติความเป็นมาของmoveวันที่กลับไปข้อเสนอย้ายเดิมในปี 2002 บทความนี้แนะนำการอ้างอิง rvalue ก่อนจากนั้นจึงแสดงวิธีการเขียนที่มีประสิทธิภาพมากขึ้น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);
}

หนึ่งจะต้องจำได้ว่า ณ จุดนี้ในประวัติศาสตร์เพียงสิ่งเดียวที่ " &&" อาจจะหมายถึงเป็นตรรกะและ ไม่มีใครคุ้นเคยกับการอ้างอิง rvalue หรือความหมายของการหล่อ lvalue เป็น rvalue (ในขณะที่ไม่ได้ทำสำเนาเหมือนที่static_cast<T>(t)ทำ) ดังนั้นผู้อ่านรหัสนี้จะคิดว่า:

ฉันรู้ว่าswapควรจะทำงานอย่างไร (คัดลอกเป็นชั่วคราวแล้วแลกเปลี่ยนค่า) แต่จุดประสงค์ของการหล่อน่าเกลียดเหล่านั้นคืออะไร?!

โปรดทราบด้วยว่าswapเป็นเพียงสแตนด์อินสำหรับอัลกอริธึมการปรับเปลี่ยนการเปลี่ยนแปลงทุกประเภท การสนทนานี้เป็นมากswapมากมีขนาดใหญ่กว่า

จากนั้นข้อเสนอจะแนะนำไวยากรณ์น้ำตาลซึ่งแทนที่static_cast<T&&>ด้วยสิ่งที่อ่านได้ง่ายขึ้นซึ่งไม่ได้บ่งบอกถึงสิ่งที่แน่นอนแต่เป็นเหตุผล :

template <class T>
void
swap(T& a, T& b)
{
    T tmp(move(a));
    a = move(b);
    b = move(tmp);
}

IE moveเป็นเพียงไวยากรณ์ของน้ำตาลstatic_cast<T&&>และตอนนี้โค้ดค่อนข้างชี้นำว่าทำไมจึงมีการร่ายเหล่านั้น: เพื่อเปิดใช้งานความหมายการย้าย!

เราต้องเข้าใจว่าในบริบทของประวัติศาสตร์มีเพียงไม่กี่คนที่เข้าใจถึงความเชื่อมโยงที่ใกล้ชิดระหว่าง rvalues ​​และ move semantics (แม้ว่ากระดาษจะพยายามอธิบายเช่นกัน):

ย้ายความหมายจะเข้ามามีบทบาทโดยอัตโนมัติเมื่อได้รับอาร์กิวเมนต์ rvalue สิ่งนี้ปลอดภัยอย่างสมบูรณ์เนื่องจากส่วนที่เหลือของโปรแกรมไม่สามารถสังเกตเห็นการย้ายทรัพยากร ( ไม่มีใครมีการอ้างอิงถึง rvalue เพื่อตรวจจับความแตกต่าง )

หากในเวลานั้นswapถูกนำเสนอเช่นนี้:

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

จากนั้นผู้คนจะมองไปที่สิ่งนั้นและพูดว่า:

แต่ทำไมคุณถึงหล่อเพื่อ rvalue?


ประเด็นหลัก:

เหมือนเดิมใช้moveไม่เคยมีใครถาม:

แต่ทำไมคุณถึงย้าย?


เมื่อหลายปีผ่านไปและข้อเสนอได้รับการปรับปรุงแนวคิดของ lvalue และ rvalue ได้รับการขัดเกลาเป็นหมวดหมู่มูลค่าที่เรามีในปัจจุบัน:

อนุกรมวิธาน

(ภาพที่ถูกขโมยลงคอจากdirkgently )

ดังนั้นวันนี้หากเราต้องการswapได้อย่างแม่นยำบอกว่าสิ่งที่จะทำแทนทำไมมันควรจะมีลักษณะเหมือน:

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

และคำถามที่ทุกคนควรถามตัวเองก็คือโค้ดด้านบนอ่านได้มากกว่าหรือน้อยกว่า:

template <class T>
void
swap(T& a, T& b)
{
    T tmp(move(a));
    a = move(b);
    b = move(tmp);
}

หรือแม้แต่ต้นฉบับ:

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

ไม่ว่าในกรณีใดโปรแกรมเมอร์ C ++ ของ Journeyman ควรรู้ว่าภายใต้ประทุนmoveนั้นไม่มีอะไรเกิดขึ้นมากไปกว่าการแคสต์ และการเริ่มต้นเขียนโปรแกรมภาษา C ++, อย่างน้อยmoveจะได้รับการแจ้งว่าเจตนาคือการย้ายจาก RHS ที่ตรงข้ามกับการคัดลอกจาก RHS แม้ว่าพวกเขาไม่เข้าใจว่าวิธีการที่จะประสบความสำเร็จ

นอกจากนี้หากโปรแกรมเมอร์ต้องการฟังก์ชันนี้ภายใต้ชื่ออื่นจะstd::moveไม่มีการผูกขาดฟังก์ชันนี้และไม่มีเวทมนตร์ภาษาที่ไม่พกพาที่เกี่ยวข้องกับการใช้งาน ตัวอย่างเช่นหากต้องการเขียนโค้ดset_value_category_to_xvalueและใช้สิ่งนั้นแทนการทำเช่นนั้นเป็นเรื่องเล็กน้อย:

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

ใน C ++ 14 จะกระชับมากยิ่งขึ้น:

template <class T>
inline
constexpr
auto&&
set_value_category_to_xvalue(T&& t) noexcept
{
    return static_cast<std::remove_reference_t<T>&&>(t);
}

ดังนั้นหากคุณมีความโน้มเอียงมาก ๆ ให้ตกแต่งstatic_cast<T&&>ตามที่คุณคิดว่าดีที่สุดและบางทีคุณอาจจะต้องพัฒนาแนวทางปฏิบัติที่ดีที่สุดใหม่ (C ++ มีการพัฒนาอย่างต่อเนื่อง)

แล้วสิ่งที่moveทำในแง่ของรหัสวัตถุที่สร้างขึ้น?

พิจารณาสิ่งนี้test:

void
test(int& i, int& j)
{
    i = j;
}

รวบรวมด้วยclang++ -std=c++14 test.cpp -O3 -Sสิ่งนี้สร้างรหัสวัตถุนี้:

__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

ตอนนี้ถ้าการทดสอบเปลี่ยนเป็น:

void
test(int& i, int& j)
{
    i = std::move(j);
}

มีอย่างแน่นอนไม่มีการเปลี่ยนแปลงที่ทุกคนในรหัสวัตถุ เราสามารถสรุปผลลัพธ์นี้เป็น: สำหรับวัตถุที่เคลื่อนย้ายได้เล็กน้อยstd::moveไม่มีผลกระทบใด ๆ

ตอนนี้ให้ดูตัวอย่างนี้:

struct X
{
    X& operator=(const X&);
};

void
test(X& i, X& j)
{
    i = j;
}

สิ่งนี้สร้าง:

__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

หากคุณดำเนินการ__ZN1XaSERKS_ผ่านc++filtมันจะสร้าง: X::operator=(X const&). ไม่แปลกใจที่นี่ ตอนนี้ถ้าการทดสอบเปลี่ยนเป็น:

void
test(X& i, X& j)
{
    i = std::move(j);
}

จากนั้นยังคงไม่มีการเปลี่ยนแปลงใด ๆในรหัสวัตถุที่สร้างขึ้น std::moveมีอะไรทำ แต่โยนjไปยัง rvalue แล้วว่า rvalue ผูกกับผู้ประกอบการที่ได้รับมอบหมายสำเนาXX

ตอนนี้ให้เพิ่มตัวดำเนินการกำหนดย้ายไปที่X:

struct X
{
    X& operator=(const X&);
    X& operator=(X&&);
};

ตอนนี้รหัสวัตถุไม่เปลี่ยนแปลง:

__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

วิ่ง__ZN1XaSEOS_ผ่านc++filtเผยให้เห็นว่าจะถูกเรียกแทนX::operator=(X&&)X::operator=(X const&)

และนั่นคือทั้งหมดที่มีให้std::move! มันจะหายไปอย่างสมบูรณ์ในเวลาทำงาน ผลกระทบเพียงอย่างเดียวคือในเวลาคอมไพล์ซึ่งอาจเปลี่ยนแปลงสิ่งที่เรียกว่าโอเวอร์โหลด


7
นี่คือแหล่งที่มาของจุดสำหรับกราฟนั้น: ฉันสร้างขึ้นใหม่digraph D { glvalue -> { lvalue; xvalue } rvalue -> { xvalue; prvalue } expression -> { glvalue; rvalue } }เพื่อประโยชน์สาธารณะ :) ดาวน์โหลดที่นี่ในชื่อ SVG
ดู

7
ที่นี่ยังเปิดให้ปั่นจักรยานอยู่ไหม ฉันขอแนะนำallow_move;)
dyp

2
@dyp ของโปรดยังอยู่movable.
Daniel Frey

6
สกอตต์เมเยอร์สแนะเปลี่ยนชื่อstd::moveไปrvalue_cast: youtube.com/...
nairware

6
เนื่องจากตอนนี้ rvalue หมายถึงทั้ง prvalues ​​และ xvalues rvalue_castความหมายของมันมีความคลุมเครือ: rvalue ประเภทใดที่ส่งคืน xvalue_castจะเป็นชื่อที่สอดคล้องกันที่นี่ น่าเสียดายที่คนส่วนใหญ่ในเวลานี้ยังไม่เข้าใจว่ากำลังทำอะไรอยู่ ในอีกไม่กี่ปีคำพูดของฉันหวังว่าจะกลายเป็นเท็จ
Howard Hinnant

20

ขอฉันทิ้งคำพูดจากคำถามที่พบบ่อย C ++ 11 ที่เขียนโดย B.Stroustrup ซึ่งเป็นคำตอบโดยตรงสำหรับคำถามของ OP:

move (x) หมายถึง "คุณสามารถถือว่า x เป็นค่า rvalue" บางทีมันอาจจะดีกว่าถ้า move () ถูกเรียกว่า rval () แต่ตอนนี้ใช้ move () มาหลายปีแล้ว

อย่างไรก็ตามฉันชอบคำถามที่พบบ่อยมาก - มันคุ้มค่าที่จะอ่าน


2
หากต้องการลอกเลียนความคิดเห็นของ @ HowardHinnant จากคำตอบอื่น: คำตอบของ Stroustrup ไม่ถูกต้องเนื่องจากขณะนี้มี rvalues ​​อยู่สองประเภทคือ prvalues ​​และ xvalues ​​และ std :: move เป็นการร่าย xvalue
einpoklum
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.