มันเป็นความถูกต้องที่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! มันจะหายไปอย่างสมบูรณ์ในเวลาทำงาน ผลกระทบเพียงอย่างเดียวคือในเวลาคอมไพล์ซึ่งอาจเปลี่ยนแปลงสิ่งที่เรียกว่าโอเวอร์โหลด
std::moveได้เคลื่อนไหวอย่างแท้จริง ..