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