ฉันเพิ่งเสร็จสิ้นการฟังวิทยุวิศวกรรมซอฟต์แวร์สัมภาษณ์พอดคาสต์กับสกอตต์เมเยอร์สเกี่ยวกับภาษา C ++ 0x คุณสมบัติใหม่ส่วนใหญ่มีเหตุผลสำหรับฉันและฉันรู้สึกตื่นเต้นจริง ๆ กับ C ++ 0x ตอนนี้ยกเว้นหนึ่งคุณลักษณะ ฉันยังไม่เข้าใจความหมาย ... มันคืออะไรกันแน่?
ฉันเพิ่งเสร็จสิ้นการฟังวิทยุวิศวกรรมซอฟต์แวร์สัมภาษณ์พอดคาสต์กับสกอตต์เมเยอร์สเกี่ยวกับภาษา C ++ 0x คุณสมบัติใหม่ส่วนใหญ่มีเหตุผลสำหรับฉันและฉันรู้สึกตื่นเต้นจริง ๆ กับ C ++ 0x ตอนนี้ยกเว้นหนึ่งคุณลักษณะ ฉันยังไม่เข้าใจความหมาย ... มันคืออะไรกันแน่?
คำตอบ:
ฉันคิดว่ามันง่ายที่สุดในการเข้าใจการย้ายความหมายด้วยโค้ดตัวอย่าง เริ่มต้นด้วยคลาสสตริงที่ง่ายมากซึ่งเก็บตัวชี้ไปยังบล็อกหน่วยความจำที่จัดสรรฮีป:
#include <cstring>
#include <algorithm>
class string
{
char* data;
public:
string(const char* p)
{
size_t size = std::strlen(p) + 1;
data = new char[size];
std::memcpy(data, p, size);
}
เนื่องจากเราเลือกจัดการหน่วยความจำด้วยตนเองเราจึงต้องปฏิบัติตามกฎสามข้อ ฉันจะเลื่อนการเขียนผู้ดำเนินการที่ได้รับมอบหมายและใช้ destructor และตัวสร้างการคัดลอกเฉพาะตอนนี้
~string()
{
delete[] data;
}
string(const string& that)
{
size_t size = std::strlen(that.data) + 1;
data = new char[size];
std::memcpy(data, that.data, size);
}
ตัวสร้างการคัดลอกกำหนดความหมายของการคัดลอกวัตถุสตริง พารามิเตอร์const string& that
ผูกกับนิพจน์ทั้งหมดของชนิดสตริงที่อนุญาตให้คุณทำสำเนาในตัวอย่างต่อไปนี้:
string a(x); // Line 1
string b(x + y); // Line 2
string c(some_function_returning_a_string()); // Line 3
ตอนนี้ความเข้าใจที่สำคัญในความหมายย้ายมา โปรดทราบว่าเฉพาะในบรรทัดแรกที่เราคัดลอกx
เป็นสำเนาลึกที่จำเป็นจริงๆเพราะเราอาจต้องการตรวจสอบในx
ภายหลังและจะประหลาดใจมากหากx
มีการเปลี่ยนแปลงอย่างใด คุณสังเกตเห็นว่าฉันเพิ่งพูดx
สามครั้ง (สี่ครั้งถ้าคุณรวมประโยคนี้) และหมายถึงวัตถุเดียวกันทุกครั้ง เราเรียกนิพจน์เช่นx
"lvalues"
อาร์กิวเมนต์ในบรรทัดที่ 2 และ 3 ไม่ใช่ lvalues แต่ rvalues เนื่องจากวัตถุสตริงต้นแบบไม่มีชื่อดังนั้นไคลเอ็นต์จึงไม่มีวิธีตรวจสอบอีกครั้งในเวลาต่อมา rvalues แสดงถึงวัตถุชั่วคราวที่ถูกทำลายในเซมิโคลอนถัดไป (เพื่อความแม่นยำมากขึ้น: ในตอนท้ายของนิพจน์เต็มรูปแบบที่มีค่า rvalue) สิ่งนี้มีความสำคัญเนื่องจากในระหว่างการเริ่มต้นb
และc
เราสามารถทำสิ่งที่เราต้องการด้วยสตริงที่มาและลูกค้าไม่สามารถบอกความแตกต่าง !
C ++ 0x แนะนำกลไกใหม่ที่เรียกว่า "การอ้างอิง rvalue" ซึ่งเหนือสิ่งอื่นใดช่วยให้เราสามารถตรวจสอบข้อโต้แย้ง rvalue ผ่านการทำงานมากเกินไป สิ่งที่เราต้องทำคือเขียนนวกรรมิกด้วยพารามิเตอร์อ้างอิง rvalue ภายในคอนสตรัคว่าเราสามารถทำอะไรที่เราต้องการมีแหล่งที่มาตราบใดที่เราทิ้งไว้ในบางรัฐที่ถูกต้อง:
string(string&& that) // string&& is an rvalue reference to a string
{
data = that.data;
that.data = nullptr;
}
พวกเราทำอะไรที่นี่? แทนที่จะทำการคัดลอกข้อมูลฮีพอย่างลึกซึ้งเราเพิ่งคัดลอกตัวชี้แล้วตั้งค่าตัวชี้ดั้งเดิมเป็นโมฆะ (เพื่อป้องกัน 'ลบ []' จากตัวทำลายของวัตถุต้นฉบับจากการปล่อยข้อมูลที่เพิ่งขโมย 'ของเรา) ในความเป็นจริงเราได้ "ขโมย" ข้อมูลที่เป็นของสตริงต้นฉบับ อีกครั้งความเข้าใจที่สำคัญคือว่าลูกค้าไม่สามารถตรวจพบว่ามีการปรับเปลี่ยนแหล่งที่มา เนื่องจากเราไม่ได้ทำสำเนาจริงๆที่นี่เราจึงเรียกตัวสร้างนี้ว่า "ตัวสร้างการย้าย" หน้าที่คือการย้ายทรัพยากรจากวัตถุหนึ่งไปอีกวัตถุหนึ่งแทนที่จะคัดลอกวัตถุ
ขอแสดงความยินดีตอนนี้คุณเข้าใจพื้นฐานของอรรถศาสตร์การย้ายแล้ว! มาดำเนินการต่อโดยใช้ตัวดำเนินการที่ได้รับมอบหมาย หากคุณไม่คุ้นเคยกับการคัดลอกและสำนวนแลกเปลี่ยนเรียนรู้และกลับมาเพราะมันเป็นสำนวน C ++ ที่ยอดเยี่ยมที่เกี่ยวข้องกับข้อยกเว้นด้านความปลอดภัย
string& operator=(string that)
{
std::swap(data, that.data);
return *this;
}
};
นั่นไงเหรอ? "การอ้างอิงค่า rvalue อยู่ที่ไหน" คุณอาจถาม "เราไม่ต้องการที่นี่!" คือคำตอบของฉัน :)
โปรดทราบว่าเราส่งพารามิเตอร์that
ตามค่าดังนั้นthat
จะต้องเริ่มต้นเช่นเดียวกับวัตถุสตริงอื่น ๆ ว่าวิธีการที่เป็นthat
ไปได้ที่จะเริ่มต้น? ในสมัยก่อนของC ++ 98คำตอบน่าจะเป็น "ตัวสร้างสำเนา" ใน C ++ 0x คอมไพเลอร์เลือกระหว่างตัวสร้างการคัดลอกและตัวสร้างการย้ายขึ้นอยู่กับว่าอาร์กิวเมนต์ของตัวดำเนินการกำหนดค่าเป็น lvalue หรือ rvalue
ดังนั้นถ้าคุณบอกว่าa = b
ตัวสร้างการคัดลอกจะเริ่มต้นthat
(เนื่องจากนิพจน์b
นั้นเป็น lvalue) และผู้ดำเนินการที่ได้รับมอบหมายจะแลกเปลี่ยนเนื้อหาด้วยสำเนาที่สร้างขึ้นใหม่ นั่นคือคำจำกัดความของการคัดลอกและสำนวนแลกเปลี่ยน - ทำสำเนาแลกเปลี่ยนเนื้อหาด้วยการคัดลอกแล้วกำจัดสำเนาโดยออกจากขอบเขต ไม่มีอะไรใหม่ที่นี่
แต่ถ้าคุณบอกว่าa = x + y
ตัวสร้างการย้ายจะเริ่มต้นthat
(เนื่องจากนิพจน์x + y
นั้นเป็นค่า rvalue) ดังนั้นจึงไม่มีการคัดลอกแบบลึกที่เกี่ยวข้อง
that
ยังคงเป็นวัตถุอิสระจากการโต้แย้ง แต่การก่อสร้างนั้นไม่สำคัญเนื่องจากข้อมูลฮีปไม่จำเป็นต้องคัดลอกเพียงแค่ย้าย ไม่จำเป็นต้องคัดลอกเพราะx + y
เป็นค่า rvalue และอีกครั้งมันก็โอเคที่จะย้ายจากวัตถุสตริงที่แสดงด้วย rvalues
เพื่อสรุปตัวสร้างการคัดลอกทำสำเนาลึกเพราะแหล่งที่มาจะต้องไม่ถูกแตะต้อง ตัวสร้างการย้ายในทางกลับกันก็สามารถคัดลอกตัวชี้แล้วตั้งตัวชี้ในแหล่งที่มาเป็นโมฆะ มันก็โอเคที่จะ "โมฆะ" วัตถุต้นฉบับในลักษณะนี้เพราะลูกค้าไม่มีวิธีการตรวจสอบวัตถุอีกครั้ง
ฉันหวังว่าตัวอย่างนี้จะเป็นประเด็นหลัก มีมากขึ้นเพื่ออ้างอิงค่าและย้ายความหมายที่ฉันตั้งใจออกเพื่อให้ง่าย หากคุณต้องการรายละเอียดเพิ่มเติมโปรดดูคำตอบของฉันเสริม
that.data = 0
ตัวละครจะถูกทำลายเร็วเกินไป (เมื่อตายชั่วคราว) และยังเป็นสองเท่า คุณต้องการขโมยข้อมูลไม่แชร์!
delete[]
บน nullptr ถูกกำหนดโดยมาตรฐาน C ++ ให้เป็นแบบไม่ใช้งาน
คำตอบแรกของฉันคือการแนะนำเบื้องต้นที่ง่ายมากในการย้ายซีแมนทิกส์และรายละเอียดมากมายถูกทิ้งไว้โดยมีจุดประสงค์เพื่อให้ง่าย อย่างไรก็ตามมีมากขึ้นในการย้ายความหมายและฉันคิดว่ามันเป็นเวลาสำหรับคำตอบที่สองเพื่อเติมช่องว่าง คำตอบแรกนั้นค่อนข้างเก่าและไม่รู้สึกถูกต้องที่จะแทนที่ด้วยข้อความที่แตกต่างอย่างสิ้นเชิง ฉันคิดว่ามันยังทำหน้าที่ได้ดีเหมือนเป็นการแนะนำครั้งแรก แต่ถ้าคุณต้องการที่จะขุดลึกอ่านเพิ่มเติม :)
สเตฟานต. Lavavej ใช้เวลาในการแสดงความคิดเห็นที่มีค่า ขอบคุณมากสเตฟาน!
ความหมายของการย้ายช่วยให้วัตถุภายใต้เงื่อนไขบางประการเพื่อเป็นเจ้าของทรัพยากรภายนอกของวัตถุอื่น นี่เป็นสิ่งสำคัญในสองวิธี:
เปลี่ยนสำเนาที่มีราคาแพงเป็นการเคลื่อนไหวที่ราคาถูก ดูคำตอบแรกของฉันสำหรับตัวอย่าง โปรดทราบว่าหากวัตถุไม่ได้จัดการทรัพยากรภายนอกอย่างน้อยหนึ่งทรัพยากร (ไม่ว่าโดยตรงหรือโดยอ้อมผ่านวัตถุสมาชิก) ความหมายของการย้ายจะไม่มีข้อได้เปรียบใด ๆ เหนือความหมายของการคัดลอก ในกรณีนั้นการคัดลอกวัตถุและย้ายวัตถุหมายถึงสิ่งเดียวกัน:
class cannot_benefit_from_move_semantics
{
int a; // moving an int means copying an int
float b; // moving a float means copying a float
double c; // moving a double means copying a double
char d[64]; // moving a char array means copying a char array
// ...
};
การใช้ประเภท "ย้ายเท่านั้น" ที่ปลอดภัย นั่นคือชนิดที่การทำสำเนาไม่สมเหตุสมผล แต่การย้ายทำได้ ตัวอย่างเช่นการล็อกตัวจัดการไฟล์และพอยน์เตอร์อัจฉริยะที่มีซีแมนทิกส์เฉพาะ หมายเหตุ: คำตอบนี้กล่าวถึงstd::auto_ptr
เทมเพลตไลบรารีมาตรฐาน C ++ 98 ที่คัดค้านซึ่งถูกแทนที่ด้วยstd::unique_ptr
ใน C ++ 11 โปรแกรมเมอร์ C ++ ระดับกลางอาจมีความคุ้นเคยอย่างน้อยstd::auto_ptr
และเนื่องจาก "ความหมายของการย้าย" แสดงขึ้นดูเหมือนว่าเป็นจุดเริ่มต้นที่ดีสำหรับการพูดคุยความหมายของการย้ายใน C ++ 11 YMMV
c ++ 98 std::auto_ptr<T>
มาตรฐานมีห้องสมุดชี้สมาร์ทที่มีความหมายเป็นเจ้าของที่ไม่ซ้ำกันเรียกว่า ในกรณีที่คุณไม่คุ้นเคยauto_ptr
จุดประสงค์ของมันคือเพื่อรับประกันว่าวัตถุที่จัดสรรแบบไดนามิกจะถูกปล่อยออกมาเสมอแม้ในกรณีที่มีข้อยกเว้น:
{
std::auto_ptr<Shape> a(new Triangle);
// ...
// arbitrary code, could throw exceptions
// ...
} // <--- when a goes out of scope, the triangle is deleted automatically
สิ่งผิดปกติเกี่ยวกับauto_ptr
คือพฤติกรรม "คัดลอก":
auto_ptr<Shape> a(new Triangle);
+---------------+
| triangle data |
+---------------+
^
|
|
|
+-----|---+
| +-|-+ |
a | p | | | |
| +---+ |
+---------+
auto_ptr<Shape> b(a);
+---------------+
| triangle data |
+---------------+
^
|
+----------------------+
|
+---------+ +-----|---+
| +---+ | | +-|-+ |
a | p | | | b | p | | | |
| +---+ | | +---+ |
+---------+ +---------+
หมายเหตุวิธีการเริ่มต้นของการb
ที่มีa
ไม่ได้คัดลอกรูปสามเหลี่ยม แต่แทนที่จะโอนความเป็นเจ้าของของรูปสามเหลี่ยมจากไปa
b
นอกจากนี้เรายังพูดว่า " a
ถูกย้ายไปที่ b
" หรือ "สามเหลี่ยมถูกย้ายจากa
ไปยัง b
" สิ่งนี้อาจฟังดูสับสนเนื่องจากรูปสามเหลี่ยมอยู่ในที่เดียวกันเสมอในหน่วยความจำ
ในการย้ายวัตถุหมายถึงการถ่ายโอนความเป็นเจ้าของทรัพยากรบางอย่างที่จัดการไปยังวัตถุอื่น
ตัวสร้างสำเนาของauto_ptr
อาจมีลักษณะเช่นนี้ (ค่อนข้างง่าย):
auto_ptr(auto_ptr& source) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
สิ่งที่อันตรายเกี่ยวกับauto_ptr
คือสิ่งที่ syntactically ดูเหมือนสำเนาจริงย้าย การพยายามเรียกฟังก์ชั่นสมาชิกที่ย้ายจากมาauto_ptr
จะทำให้เกิดพฤติกรรมที่ไม่ได้กำหนดดังนั้นคุณต้องระวังไม่ให้ใช้auto_ptr
หลังจากที่ย้ายจาก:
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
แต่auto_ptr
ไม่อันตรายเสมอไป ฟังก์ชั่นจากโรงงานเป็นกรณีการใช้งานที่สมบูรณ์แบบสำหรับauto_ptr
:
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
สังเกตว่าทั้งสองตัวอย่างทำตามรูปแบบวากยสัมพันธ์เดียวกันอย่างไร:
auto_ptr<Shape> variable(expression);
double area = expression->area();
และหนึ่งในนั้นก็เรียกใช้พฤติกรรมที่ไม่ได้กำหนดในขณะที่อีกคนหนึ่งไม่ได้ ดังนั้นความแตกต่างระหว่างการแสดงออกa
และmake_triangle()
คืออะไร? พวกเขาทั้งสองประเภทเดียวกันไม่ได้เหรอ? อันที่จริงพวกเขามี แต่พวกเขามีความแตกต่างกันหมวดมูลค่า
เห็นได้ชัดว่าจะต้องมีความแตกต่างอย่างลึกซึ้งระหว่างการแสดงออกa
ซึ่งหมายถึงauto_ptr
ตัวแปรและการแสดงออกmake_triangle()
ซึ่งหมายถึงการเรียกใช้ฟังก์ชั่นที่ส่งกลับauto_ptr
ค่าโดยการสร้างauto_ptr
วัตถุชั่วคราวสดทุกครั้งที่มันถูกเรียก a
เป็นตัวอย่างของการเป็นนักการlvalueในขณะที่make_triangle()
เป็นตัวอย่างของการเป็นนักการrvalue
การย้ายจาก lvalues เช่นเป็นa
สิ่งที่อันตรายเพราะเราสามารถลองเรียกฟังก์ชันสมาชิกในภายหลังโดยa
เรียกใช้พฤติกรรมที่ไม่ได้กำหนด ในทางกลับกันการย้ายจากค่าเช่นmake_triangle()
มีความปลอดภัยอย่างสมบูรณ์เพราะหลังจากตัวสร้างสำเนาทำงานของมันแล้วเราไม่สามารถใช้งานชั่วคราวได้อีก ไม่มีการแสดงออกที่แสดงว่าชั่วคราว ถ้าเราเพียงแค่เขียนmake_triangle()
อีกครั้งที่เราได้รับแตกต่างกันชั่วคราว ในความเป็นจริงการย้ายจากชั่วคราวไปแล้วในบรรทัดถัดไป:
auto_ptr<Shape> c(make_triangle());
^ the moved-from temporary dies right here
โปรดทราบว่าตัวอักษรl
และr
มีต้นกำเนิดประวัติศาสตร์ในด้านซ้ายและด้านขวามือของการมอบหมาย สิ่งนี้ไม่เป็นความจริงใน C ++ อีกต่อไปเนื่องจากมีค่าที่ไม่สามารถปรากฏทางด้านซ้ายมือของการมอบหมาย (เช่นอาร์เรย์หรือประเภทที่ผู้ใช้กำหนดเองโดยไม่มีตัวดำเนินการกำหนดค่า) และมีค่าที่สามารถ (ค่าทั้งหมดของชั้นเรียนทั้งหมด กับผู้ประกอบการที่ได้รับมอบหมาย)
rvalue ของประเภทชั้นเรียนคือการแสดงออกที่มีการประเมินผลสร้างวัตถุชั่วคราว ภายใต้สถานการณ์ปกติไม่มีนิพจน์อื่นภายในขอบเขตเดียวกันแสดงถึงวัตถุชั่วคราวเดียวกัน
ตอนนี้เราเข้าใจว่าการเคลื่อนย้ายจาก lvalues อาจเป็นอันตราย แต่การย้ายจาก rvalues นั้นไม่เป็นอันตราย หาก C ++ มีภาษารองรับเพื่อแยกความแตกต่างของข้อโต้แย้ง lvalue จากข้อโต้แย้ง rvalue เราสามารถห้ามการย้ายจาก lvalues อย่างสมบูรณ์หรืออย่างน้อยก็ทำให้การย้ายจาก lvalues ชัดเจนที่ไซต์การโทรเพื่อให้เราไม่ย้ายโดยไม่ตั้งใจอีกต่อไป
คำตอบของ C ++ 11 สำหรับปัญหานี้คือการอ้างอิงที่คุ้มค่า การอ้างอิง rvalue เป็นชนิดใหม่ของการอ้างอิงที่ผูกเท่านั้นที่จะ rvalues X&&
และไวยากรณ์คือ อ้างอิงเก่าที่ดีX&
เป็นที่รู้จักกันตอนนี้เป็นอ้างอิง lvalue (หมายเหตุว่าX&&
เป็นไม่ได้มีการอ้างอิงถึงการอ้างอิง; ไม่มีสิ่งดังกล่าวใน C ++.)
ถ้าเราโยนconst
ลงไปมิกซ์เรามีการอ้างอิงสี่แบบที่ต่างกัน ประเภทของการแสดงออกประเภทใดX
พวกเขาสามารถผูกกับ?
lvalue const lvalue rvalue const rvalue
---------------------------------------------------------
X& yes
const X& yes yes yes yes
X&& yes
const X&& yes yes
const X&&
ในทางปฏิบัติคุณสามารถลืมเกี่ยวกับ การ จำกัด ให้อ่านจากค่า rvalues นั้นไม่มีประโยชน์มากนัก
การอ้างอิง rvalue เป็นการอ้างอิง
X&&
ชนิดใหม่ที่ผูกกับค่า rvalue เท่านั้น
การอ้างอิงที่คุ้มค่าผ่านหลายเวอร์ชัน ตั้งแต่รุ่น 2.1 การอ้างอิง rvalue X&&
ยังผูกกับทุกประเภทมูลค่าของประเภทที่แตกต่างกันY
ให้มีการแปลงนัยจากการY
X
ในกรณีดังX
กล่าวจะมีการสร้างชนิดชั่วคราวและการอ้างอิง rvalue จะเชื่อมโยงกับชั่วคราว:
void some_function(std::string&& r);
some_function("hello world");
ในตัวอย่างข้างต้น"hello world"
เป็น lvalue const char[12]
ประเภท นับตั้งแต่มีการแปลงนัยจากconst char[12]
ผ่านconst char*
ไปstd::string
เป็นชั่วคราวของประเภทstd::string
จะถูกสร้างขึ้นและr
ถูกผูกไว้ชั่วคราวที่ นี่เป็นหนึ่งในกรณีที่ความแตกต่างระหว่าง rvalues (นิพจน์) และ tempages (วัตถุ) นั้นเบลอเล็กน้อย
ตัวอย่างการใช้งานของฟังก์ชั่นที่มีX&&
พารามิเตอร์ที่เป็นตัวสร้างย้าย X::X(X&& source)
โดยมีวัตถุประสงค์คือเพื่อโอนความเป็นเจ้าของของทรัพยากรที่มีการจัดการจากแหล่งลงในวัตถุปัจจุบัน
ใน C ++ 11 std::auto_ptr<T>
ถูกแทนที่ด้วยstd::unique_ptr<T>
ซึ่งใช้ประโยชน์จากการอ้างอิง rvalue unique_ptr
ฉันจะมีการพัฒนาและหารือฉบับง่าย อันดับแรกเราใส่แค็ปซูลตัวชี้แบบดิบและโอเวอร์โหลดตัวดำเนินการ->
และ*
ดังนั้นคลาสของเราจึงรู้สึกเหมือนตัวชี้:
template<typename T>
class unique_ptr
{
T* ptr;
public:
T* operator->() const
{
return ptr;
}
T& operator*() const
{
return *ptr;
}
ตัวสร้างใช้ความเป็นเจ้าของของวัตถุและ destructor ลบมัน:
explicit unique_ptr(T* p = nullptr)
{
ptr = p;
}
~unique_ptr()
{
delete ptr;
}
ตอนนี้ส่วนที่น่าสนใจผู้สร้างย้าย:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
ตัวสร้างการย้ายนี้ทำสิ่งที่ตัวauto_ptr
สร้างการคัดลอกทำ แต่สามารถให้ค่า rvalues ได้เท่านั้น:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
บรรทัดที่สองไม่สามารถคอมไพล์ได้เนื่องจากa
เป็น lvalue แต่พารามิเตอร์unique_ptr&& source
สามารถผูกกับ rvalues เท่านั้น ตรงนี้เป็นสิ่งที่เราต้องการ การเคลื่อนไหวที่อันตรายไม่ควรกระทำโดยปริยาย บรรทัดที่สามคอมไพล์ได้ดีเพราะmake_triangle()
เป็นค่า rvalue c
นวกรรมิกย้ายจะโอนกรรมสิทธิ์จากการชั่วคราว นี่คือสิ่งที่เราต้องการ
ตัวสร้างการเคลื่อนย้ายถ่ายโอนความเป็นเจ้าของของทรัพยากรที่มีการจัดการลงในวัตถุปัจจุบัน
ชิ้นสุดท้ายที่หายไปคือผู้ประกอบการที่ได้รับมอบหมายย้าย หน้าที่คือการปล่อยทรัพยากรเก่าและรับทรัพยากรใหม่จากการโต้แย้ง:
unique_ptr& operator=(unique_ptr&& source) // note the rvalue reference
{
if (this != &source) // beware of self-assignment
{
delete ptr; // release the old resource
ptr = source.ptr; // acquire the new resource
source.ptr = nullptr;
}
return *this;
}
};
สังเกตว่าการใช้งานตัวดำเนินการกำหนดค่าการย้ายนี้ซ้ำตรรกะของทั้งตัวทำลายและตัวสร้างการย้าย คุณคุ้นเคยกับสำนวนการคัดลอกและแลกเปลี่ยนหรือไม่? นอกจากนี้ยังสามารถนำมาใช้เพื่อย้ายความหมายเป็นสำนวนการย้ายและแลกเปลี่ยน:
unique_ptr& operator=(unique_ptr source) // note the missing reference
{
std::swap(ptr, source.ptr);
return *this;
}
};
ตอนนี้source
เป็นตัวแปรประเภทunique_ptr
มันจะเริ่มต้นได้โดยตัวสร้างการย้าย; นั่นคืออาร์กิวเมนต์จะถูกย้ายไปยังพารามิเตอร์ อาร์กิวเมนต์ยังจำเป็นต้องเป็นค่า rvalue เนื่องจากตัวสร้างการย้ายเองมีพารามิเตอร์อ้างอิง rvalue เมื่อการควบคุมการไหลถึงรั้งปิดoperator=
, source
ไปจากขอบเขตปล่อยทรัพยากรเก่าโดยอัตโนมัติ
ผู้ประกอบการที่ได้รับมอบหมายย้ายโอนความเป็นเจ้าของของทรัพยากรที่มีการจัดการลงในวัตถุปัจจุบันปล่อยทรัพยากรเก่า สำนวนการย้ายและสลับทำให้การใช้งานง่ายขึ้น
บางครั้งเราต้องการย้ายจาก lvalues นั่นคือบางครั้งเราต้องการให้คอมไพเลอร์รักษา lvalue ราวกับว่าเป็น rvalue ดังนั้นจึงสามารถเรียกใช้ตัวสร้างการย้ายแม้ว่ามันอาจจะไม่ปลอดภัยก็ตาม เพื่อจุดประสงค์นี้, C ++ 11 ข้อเสนอฟังก์ชั่นมาตรฐานห้องสมุดแม่แบบที่เรียกว่าภายในส่วนหัวstd::move
<utility>
ชื่อนี้ค่อนข้างโชคร้ายเพราะstd::move
เพียงแค่ใช้ค่า lvalue กับค่า rvalue มันไม่ได้ย้ายอะไรด้วยตัวเอง มันทำให้เคลื่อนไหวได้ บางทีมันควรจะมีชื่อstd::cast_to_rvalue
หรือstd::enable_move
แต่เราติดอยู่กับชื่อตอนนี้
นี่คือวิธีที่คุณย้ายจาก lvalue อย่างชัดเจน:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
โปรดทราบว่าหลังจากบรรทัดที่สามa
ไม่มีเจ้าของรูปสามเหลี่ยมอีกต่อไป ก็ไม่เป็นไรเพราะโดยชัดเจนเขียนstd::move(a)
เราทำให้ความตั้งใจของเราที่ชัดเจน: "คอนสตรัคที่รักทำสิ่งที่คุณต้องการด้วยa
เพื่อที่จะเริ่มต้นc
ผมไม่สนใจเกี่ยวกับa
อีกต่อไปรู้สึกอิสระที่จะมีวิธีของคุณด้วย. a
."
std::move(some_lvalue)
ปลดล็อก lvalue ให้กับ rvalue ดังนั้นจึงทำให้สามารถเคลื่อนที่ได้ในภายหลัง
โปรดทราบว่าแม้ว่าจะstd::move(a)
เป็นค่า rvalue การประเมินค่าจะไม่สร้างวัตถุชั่วคราว ปริศนานี้บังคับให้คณะกรรมการแนะนำหมวดหมู่คุณค่าที่สาม สิ่งที่สามารถผูกมัดกับการอ้างอิงค่า rvalue แม้ว่าจะไม่ใช่ rvalue ในความหมายดั้งเดิมเรียกว่าxvalue (eXpiring ตามตัวอักษร) ค่า rvalues ดั้งเดิมถูกเปลี่ยนชื่อเป็นprvalues (ค่า r Pure แบบบริสุทธิ์)
ทั้ง prvalues และ xvalues เป็น rvalues Xvalues และ lvalues มีทั้งglvalues (lvalues ทั่วไป) ความสัมพันธ์นั้นง่ายต่อการเข้าใจด้วยไดอะแกรม:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
โปรดทราบว่าเฉพาะค่า xvalues ที่เป็นจริงเท่านั้น ส่วนที่เหลือเป็นเพียงการเปลี่ยนชื่อและการจัดกลุ่ม
c ++ 98 rvalues เรียกว่า prvalues ใน C ++ 11 ทางใจแทนที่ "rvalue" ที่เกิดขึ้นทั้งหมดในย่อหน้าก่อนหน้าด้วย "prvalue"
จนถึงตอนนี้เราได้เห็นการเคลื่อนไหวในตัวแปรท้องถิ่นและเป็นพารามิเตอร์การทำงาน แต่การเคลื่อนที่ก็สามารถทำได้ในทิศทางตรงกันข้าม หากฟังก์ชันส่งคืนโดยค่าวัตถุบางอย่างที่ไซต์การโทร (อาจเป็นตัวแปรในตัวเครื่องหรือชั่วคราว แต่อาจเป็นวัตถุชนิดใดก็ได้) เริ่มต้นด้วยนิพจน์หลังจากreturn
คำสั่งเป็นอาร์กิวเมนต์ไปยังตัวสร้างการย้าย:
unique_ptr<Shape> make_triangle()
{
return unique_ptr<Shape>(new Triangle);
} \-----------------------------/
|
| temporary is moved into c
|
v
unique_ptr<Shape> c(make_triangle());
บางทีน่าแปลกใจที่วัตถุอัตโนมัติ (ตัวแปรท้องถิ่นที่ไม่ได้ประกาศไว้static
) สามารถย้ายออกจากฟังก์ชันโดยปริยายได้ :
unique_ptr<Shape> make_square()
{
unique_ptr<Shape> result(new Square);
return result; // note the missing std::move
}
ตัวสร้างการย้ายมาทำไมยอมรับ lvalue result
เป็นอาร์กิวเมนต์? ขอบเขตของresult
กำลังจะสิ้นสุดและจะถูกทำลายระหว่างการคลายสแต็ก ไม่มีใครสามารถบ่นได้หลังจากนั้นresult
มีการเปลี่ยนแปลงอย่างใด; เมื่อโฟลว์คอนโทรลกลับมาที่ผู้โทรresult
ไม่มีอยู่อีกต่อไป! สำหรับเหตุผลที่ C ++ 11 std::move
มีกฎพิเศษที่ช่วยให้กลับวัตถุอัตโนมัติจากฟังก์ชั่นโดยไม่ต้องเขียน ในความเป็นจริงคุณไม่ควรใช้std::move
เพื่อย้ายวัตถุอัตโนมัติออกจากฟังก์ชั่นเนื่องจากจะเป็นการยับยั้ง "การปรับค่าตอบแทนที่มีชื่อ" (NRVO)
อย่าใช้
std::move
เพื่อย้ายวัตถุอัตโนมัติออกจากฟังก์ชั่น
โปรดทราบว่าในทั้งสองฟังก์ชั่นจากโรงงานชนิดส่งคืนจะเป็นค่าไม่ใช่การอ้างอิง rvalue การอ้างอิง Rvalue ยังคงเป็นการอ้างอิงและเช่นเคยคุณไม่ควรส่งคืนการอ้างอิงไปยังวัตถุอัตโนมัติ ผู้เรียกจะลงท้ายด้วยการอ้างอิงห้อยถ้าคุณหลอกให้คอมไพเลอร์ในการรับรหัสของคุณเช่นนี้
unique_ptr<Shape>&& flawed_attempt() // DO NOT DO THIS!
{
unique_ptr<Shape> very_bad_idea(new Square);
return std::move(very_bad_idea); // WRONG!
}
ไม่ส่งคืนวัตถุอัตโนมัติโดยการอ้างอิงค่า การเคลื่อนย้ายทำได้โดยตัวสร้างการย้ายเท่านั้นไม่ใช่โดย
std::move
และไม่เพียง แต่ผูกค่า rvalue กับการอ้างอิง rvalue
ไม่ช้าก็เร็วคุณจะต้องเขียนโค้ดดังนี้:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
โดยทั่วไปคอมไพเลอร์จะบ่นว่าparameter
เป็น lvalue หากคุณดูประเภทของมันคุณจะเห็นการอ้างอิง rvalue แต่การอ้างอิง rvalue นั้นหมายถึง "การอ้างอิงที่ถูกผูกไว้กับ rvalue"; มันไม่ได้หมายความว่าการอ้างอิงนั้นเป็นค่าที่แท้จริง! แท้จริงแล้วparameter
เป็นเพียงตัวแปรธรรมดาที่มีชื่อ คุณสามารถใช้parameter
บ่อยเท่าที่คุณต้องการภายในร่างกายของนวกรรมิกและมันมักจะหมายถึงวัตถุเดียวกัน การย้ายโดยปริยายจากมันจะเป็นอันตรายดังนั้นภาษาจึงห้าม
การอ้างอิง rvalue ที่มีชื่อคือ lvalue เช่นเดียวกับตัวแปรอื่น ๆ
วิธีแก้ปัญหาคือการเปิดใช้งานการย้ายด้วยตนเอง:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(std::move(parameter)) // note the std::move
{}
};
คุณสามารถยืนยันว่าจะไม่ใช้อีกต่อไปหลังจากที่เริ่มต้นของparameter
member
เหตุใดจึงไม่มีกฎพิเศษในการแทรกอย่างเงียบ ๆstd::move
เช่นเดียวกับค่าส่งคืน อาจเป็นเพราะมันจะเป็นภาระมากเกินไปในการใช้งานคอมไพเลอร์ ตัวอย่างเช่นเกิดอะไรขึ้นถ้าเนื้อความของตัวสร้างอยู่ในหน่วยการแปลอื่น ในทางกลับกันกฎคืนค่าจะต้องตรวจสอบตารางสัญลักษณ์เพื่อพิจารณาว่าตัวระบุหลังจากreturn
คำหลักแสดงถึงวัตถุอัตโนมัติหรือไม่
คุณยังสามารถผ่านparameter
ค่า สำหรับประเภทย้ายอย่างเดียวunique_ptr
ดูเหมือนว่ายังไม่มีสำนวนที่สร้างไว้แล้ว โดยส่วนตัวแล้วฉันชอบที่จะส่งผ่านค่าเพราะมันทำให้เกิดความยุ่งเหยิงในอินเตอร์เฟส
C ++ 98 ประกาศหน้าที่พิเศษสมาชิกสามฟังก์ชันตามความต้องการนั่นคือเมื่อพวกเขาต้องการที่ไหนสักแห่ง: ตัวสร้างการคัดลอกตัวดำเนินการกำหนดค่าการคัดลอกและ destructor
X::X(const X&); // copy constructor
X& X::operator=(const X&); // copy assignment operator
X::~X(); // destructor
การอ้างอิงที่คุ้มค่าผ่านหลายเวอร์ชัน ตั้งแต่เวอร์ชัน 3.0, C ++ 11 ประกาศเพิ่มสมาชิกฟังก์ชันพิเศษสองตัวตามต้องการ: ตัวสร้างการย้ายและตัวดำเนินการกำหนดค่าการย้าย โปรดทราบว่าทั้ง VC10 และ VC11 นั้นไม่เป็นไปตามเวอร์ชัน 3.0 ดังนั้นคุณจะต้องใช้มันเอง
X::X(X&&); // move constructor
X& X::operator=(X&&); // move assignment operator
ฟังก์ชันพิเศษสมาชิกใหม่ทั้งสองนี้จะถูกประกาศโดยปริยายเฉพาะหากไม่มีฟังก์ชันสมาชิกพิเศษใด ๆ ที่ประกาศด้วยตนเอง นอกจากนี้หากคุณประกาศตัวสร้างการย้ายของคุณเองหรือตัวดำเนินการกำหนดย้ายตัวสร้างการคัดลอกหรือตัวดำเนินการกำหนดค่าการคัดลอกจะถูกประกาศโดยปริยาย
กฎเหล่านี้มีความหมายในทางปฏิบัติอย่างไร
ถ้าคุณเขียนคลาสโดยไม่มีทรัพยากรที่ไม่มีการจัดการคุณไม่จำเป็นต้องประกาศฟังก์ชันสมาชิกพิเศษห้ารายการด้วยตนเองและคุณจะได้รับสำเนาความหมายที่ถูกต้องและย้ายซีแมนทิกส์ฟรี มิฉะนั้นคุณจะต้องใช้ฟังก์ชั่นสมาชิกพิเศษด้วยตัวเอง แน่นอนถ้าคลาสของคุณไม่ได้รับประโยชน์จากความหมายของการย้ายคุณไม่จำเป็นต้องใช้การดำเนินการย้ายแบบพิเศษ
โปรดทราบว่าผู้ประกอบการที่ได้รับมอบหมายการคัดลอกและผู้ประกอบการได้รับมอบหมายสามารถหลอมรวมเป็นผู้ประกอบการที่ได้รับมอบหมายแบบครบวงจรเดียวรับอาร์กิวเมนต์โดยค่า:
X& X::operator=(X source) // unified assignment operator
{
swap(source); // see my first answer for an explanation
return *this;
}
ด้วยวิธีนี้จำนวนฟังก์ชั่นสมาชิกพิเศษที่จะใช้ลดลงจากห้าถึงสี่ มีข้อเสียระหว่างข้อยกเว้นด้านความปลอดภัยและประสิทธิภาพที่นี่ แต่ฉันไม่ใช่ผู้เชี่ยวชาญในเรื่องนี้
พิจารณาเทมเพลตฟังก์ชันต่อไปนี้:
template<typename T>
void foo(T&&);
คุณอาจคาดว่าT&&
จะผูกกับค่าเพียงเพราะอย่างรวดเร็วก่อนดูเหมือนว่าการอ้างอิงค่า ตามที่ปรากฎว่าT&&
ยังผูกกับ lvalues ด้วย:
foo(make_triangle()); // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a); // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&
ถ้าอาร์กิวเมนต์เป็น rvalue ประเภทX
, T
จะสรุปได้ว่าจะเป็นX
จึงหมายความว่าT&&
X&&
นี่คือสิ่งที่ทุกคนคาดหวัง แต่ถ้าอาร์กิวเมนต์เป็น lvalue ประเภทX
เนื่องจากกฎพิเศษT
จะสรุปได้ว่าจะเป็นX&
ด้วยเหตุนี้จะหมายถึงสิ่งที่ต้องการT&&
X& &&
แต่เนื่องจากภาษา C ++ ยังคงมีความคิดของการอ้างอิงถึงการอ้างอิงไม่มีชนิดX& &&
จะทรุดX&
ลงไป สิ่งนี้อาจฟังดูสับสนและไร้ประโยชน์ในตอนแรก แต่การยุบอ้างอิงเป็นสิ่งจำเป็นสำหรับการส่งต่อที่สมบูรณ์แบบ (ซึ่งจะไม่มีการกล่าวถึงที่นี่)
T&& ไม่ใช่การอ้างอิงค่า แต่เป็นการส่งต่อการอ้างอิง นอกจากนี้ยังผูกกับ lvalues ซึ่งในกรณีนี้
T
และT&&
เป็นการอ้างอิง lvalue ทั้งคู่
หากคุณต้องการ จำกัด แม่แบบฟังก์ชั่นเพื่อให้ค่าคุณสามารถรวมSFINAEกับลักษณะประเภท:
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);
หลังจากที่คุณเข้าใจการอ้างอิงแบบยุบนี่คือวิธีstd::move
การใช้งาน
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
ตามที่คุณเห็นmove
ยอมรับพารามิเตอร์ทุกชนิดด้วยการอ้างอิงการส่งต่อT&&
และจะส่งคืนการอ้างอิง rvalue std::remove_reference<T>::type
โทร meta ฟังก์ชั่นเป็นสิ่งที่จำเป็นเพราะมิฉะนั้นสำหรับ lvalues ประเภทX
ประเภทผลตอบแทนจะเป็นซึ่งจะพังครืนลงมาX& &&
X&
เนื่องจากt
เป็น lvalue เสมอ (โปรดจำไว้ว่าการอ้างอิง rvalue ที่มีชื่อคือ lvalue) แต่เราต้องการผูกt
กับการอ้างอิง rvalue เราจึงt
ต้องส่งกลับไปที่ประเภทการส่งคืนที่ถูกต้องอย่างชัดเจน การเรียกใช้ฟังก์ชันที่ส่งคืนการอ้างอิง rvalue นั้นคือ xvalue ตอนนี้คุณรู้แล้วว่าค่ามาจากไหน;)
การเรียกใช้ฟังก์ชันที่ส่งคืนการอ้างอิง rvalue เช่น
std::move
คือ xvalue
โปรดทราบว่าการส่งคืนโดยการอ้างอิง rvalue นั้นใช้ได้ในตัวอย่างนี้เนื่องจากt
ไม่ได้หมายถึงวัตถุอัตโนมัติ แต่แทนวัตถุที่ถูกส่งผ่านโดยผู้เรียกแทน
ย้ายหมายจะขึ้นอยู่กับการอ้างอิง rvalue
ค่า rvalue เป็นวัตถุชั่วคราวซึ่งจะถูกทำลายเมื่อสิ้นสุดการแสดงออก ใน C ++ ปัจจุบัน rvalues ผูกเฉพาะกับconst
การอ้างอิง C ++ 1x จะอนุญาตconst
การอ้างอิงที่ไม่ใช่ค่าที่ถูกสะกดคำT&&
ซึ่งเป็นการอ้างอิงไปยังวัตถุ rvalue
เนื่องจากค่า rvalue กำลังจะตายในตอนท้ายของนิพจน์คุณสามารถขโมยข้อมูลได้ แทนที่จะคัดลอกลงในวัตถุอื่นคุณย้ายข้อมูลลงไป
class X {
public:
X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
: data_()
{
// since 'x' is an rvalue object, we can steal its data
this->swap(std::move(rhs));
// this will leave rhs with the empty data
}
void swap(X&& rhs);
// ...
};
// ...
X f();
X x = f(); // f() returns result as rvalue, so this calls move-ctor
ในโค้ดข้างต้นโดยมีคอมไพเลอร์เก่าผลมาจากการf()
ถูกคัดลอกลงในx
การใช้X
ของตัวสร้างสำเนา หากคอมไพเลอร์ของคุณรองรับซีแมนทิกส์ของการย้ายและX
มีตัวสร้างการย้ายนั่นจะถูกเรียกแทน เนื่องจากrhs
ข้อโต้แย้งของมันคือค่าrvalueเรารู้ว่าไม่จำเป็นอีกต่อไปและเราสามารถขโมยค่าของมันได้
ดังนั้นค่าจะถูกย้ายจากชั่วคราวที่ไม่มีชื่อที่ส่งคืนจากf()
ไปยังx
(ในขณะที่ข้อมูลของที่x
เริ่มต้นไปที่ว่างเปล่าX
จะถูกย้ายไปที่ชั่วคราวซึ่งจะถูกทำลายหลังจากการมอบหมาย)
this->swap(std::move(rhs));
เพราะการอ้างอิงชื่อ rvalue เป็น lvalues
rhs
เป็นlvalueX::X(X&& rhs)
ในบริบทของ คุณต้องโทรหาstd::move(rhs)
ค่า rvalue แต่แบบนี้จะทำให้คำตอบนั้นเป็นคำตอบ
สมมติว่าคุณมีฟังก์ชั่นที่ส่งคืนวัตถุจำนวนมาก:
Matrix multiply(const Matrix &a, const Matrix &b);
เมื่อคุณเขียนโค้ดเช่นนี้:
Matrix r = multiply(a, b);
จากนั้นคอมไพเลอร์ C ++ สามัญจะสร้างวัตถุชั่วคราวสำหรับผลลัพธ์ของการmultiply()
เรียกตัวสร้างการคัดลอกเพื่อเริ่มต้นr
แล้วทำลายค่าส่งคืนชั่วคราว ความหมายของการย้ายใน C ++ 0x อนุญาตให้ "ตัวสร้างการย้าย" ถูกเรียกเพื่อเริ่มต้นr
โดยการคัดลอกเนื้อหาแล้วละทิ้งค่าชั่วคราวโดยไม่ต้องทำลายมัน
สิ่งนี้มีความสำคัญเป็นพิเศษหาก (เช่นอาจเป็นMatrix
ตัวอย่างด้านบน) วัตถุที่ถูกคัดลอกจะจัดสรรหน่วยความจำเพิ่มเติมในฮีปเพื่อจัดเก็บการแสดงภายใน ตัวสร้างสำเนาจะต้องทำสำเนาเต็มรูปแบบของการเป็นตัวแทนภายในหรือใช้การนับการอ้างอิงและความหมายการคัดลอกเมื่อเขียน interally ตัวสร้างการย้ายจะปล่อยให้หน่วยความจำฮีปอยู่คนเดียวและเพียงแค่คัดลอกตัวชี้ภายในMatrix
วัตถุ
หากคุณสนใจในคำอธิบายเชิงลึกของการย้ายความหมายที่ดีจริงๆฉันขอแนะนำให้อ่านเอกสารต้นฉบับเกี่ยวกับพวกเขา"ข้อเสนอเพื่อเพิ่มความหมายของการย้ายความหมายกับภาษา C ++"
เข้าถึงได้ง่ายและอ่านง่ายและทำให้เป็นกรณีที่ยอดเยี่ยมสำหรับผลประโยชน์ที่พวกเขาเสนอ มีเอกสารอื่น ๆ ที่ใหม่กว่าและทันสมัยเกี่ยวกับซีแมนติกส์การย้ายที่มีอยู่ในเว็บไซต์ WG21แต่บทความนี้น่าจะตรงไปตรงมาที่สุดเพราะมันเข้าใกล้สิ่งต่าง ๆ จากมุมมองระดับบนสุดและไม่ได้ลงลึกเข้าไปในรายละเอียดภาษา
ความหมายของการย้ายคือการถ่ายโอนทรัพยากรแทนที่จะคัดลอกเมื่อไม่มีใครต้องการค่าแหล่งข้อมูลอีกต่อไป
ใน C ++ 03 วัตถุมักจะถูกคัดลอกเท่านั้นที่จะถูกทำลายหรือได้รับมอบหมายก่อนที่รหัสใด ๆ จะใช้ค่าอีกครั้ง ตัวอย่างเช่นเมื่อคุณกลับตามมูลค่าจากฟังก์ชั่น - เว้นแต่ RVO เริ่มเล่น - ค่าที่คุณส่งคืนจะถูกคัดลอกไปยังกรอบสแต็คของผู้โทรแล้วมันจะออกนอกขอบเขตและถูกทำลาย นี่เป็นเพียงหนึ่งในหลายตัวอย่าง: ดูการส่งต่อค่าเมื่อวัตถุต้นทางเป็นแบบชั่วคราวอัลกอริธึมแบบsort
ที่เพิ่งจัดเรียงรายการใหม่การจัดสรรใหม่vector
เมื่อcapacity()
เกินค่า ฯลฯ
เมื่อคู่คัดลอก / ทำลายดังกล่าวมีราคาแพงมักจะเป็นเพราะวัตถุเป็นเจ้าของทรัพยากรเฮฟวี่เวทบางอย่าง ตัวอย่างเช่นvector<string>
อาจเป็นเจ้าของบล็อกหน่วยความจำที่จัดสรรแบบไดนามิกที่มีอาร์เรย์ของstring
วัตถุแต่ละรายการมีหน่วยความจำแบบไดนามิกของตัวเอง การคัดลอกวัตถุดังกล่าวมีค่าใช้จ่ายสูง: คุณต้องจัดสรรหน่วยความจำใหม่สำหรับบล็อกที่จัดสรรแบบไดนามิกแต่ละบล็อกในแหล่งที่มาและคัดลอกค่าทั้งหมดข้าม จากนั้นคุณต้องยกเลิกการจัดสรรหน่วยความจำทั้งหมดที่คุณเพิ่งคัดลอก อย่างไรก็ตามการย้ายวิธีการที่มีขนาดใหญ่vector<string>
เพียงแค่คัดลอกพอยน์เตอร์สองสามตัว (ที่อ้างถึงบล็อกหน่วยความจำแบบไดนามิก) ไปยังปลายทางและ zeroing พวกมันออกในแหล่งที่มา
ในแง่ง่าย (ปฏิบัติ):
การคัดลอกวัตถุหมายถึงการคัดลอกสมาชิก "คงที่" และเรียกnew
ผู้ประกอบการหาวัตถุแบบไดนามิก ขวา?
class A
{
int i, *p;
public:
A(const A& a) : i(a.i), p(new int(*a.p)) {}
~A() { delete p; }
};
อย่างไรก็ตามในการย้ายวัตถุ (ฉันทำซ้ำในมุมมองของภาคปฏิบัติ) หมายถึงเฉพาะการคัดลอกตัวชี้ของวัตถุแบบไดนามิกและไม่สร้างวัตถุใหม่
แต่นั่นไม่เป็นอันตรายหรือไม่? แน่นอนคุณสามารถทำลายวัตถุแบบไดนามิกสองครั้ง (ความผิดส่วนแบ่ง) ดังนั้นเพื่อหลีกเลี่ยงปัญหาดังกล่าวคุณควร "โมฆะ" ตัวชี้แหล่งที่มาเพื่อหลีกเลี่ยงการทำลายพวกเขาสองครั้ง:
class A
{
int i, *p;
public:
// Movement of an object inside a copy constructor.
A(const A& a) : i(a.i), p(a.p)
{
a.p = nullptr; // pointer invalidated.
}
~A() { delete p; }
// Deleting NULL, 0 or nullptr (address 0x0) is safe.
};
ตกลง แต่ถ้าฉันย้ายวัตถุวัตถุต้นทางจะไร้ประโยชน์ใช่ไหม? แน่นอน แต่ในบางสถานการณ์มันมีประโยชน์มาก สิ่งที่ชัดเจนที่สุดคือเมื่อฉันเรียกใช้ฟังก์ชันที่มีวัตถุที่ไม่ระบุตัวตน (วัตถุชั่วคราว, วัตถุ rvalue, ... , คุณสามารถเรียกมันด้วยชื่ออื่น):
void heavyFunction(HeavyType());
ในสถานการณ์ดังกล่าวจะมีการสร้างวัตถุที่ไม่ระบุชื่อคัดลอกไปยังพารามิเตอร์ฟังก์ชันแล้วลบทิ้งในภายหลัง ดังนั้นที่นี่จะดีกว่าที่จะย้ายวัตถุเพราะคุณไม่ต้องการวัตถุที่ไม่ระบุชื่อและคุณสามารถประหยัดเวลาและหน่วยความจำ
สิ่งนี้นำไปสู่แนวคิดของการอ้างอิง "rvalue" พวกเขามีอยู่ใน C ++ 11 เท่านั้นที่จะตรวจสอบว่าวัตถุที่ได้รับไม่ระบุชื่อหรือไม่ ฉันคิดว่าคุณรู้อยู่แล้วว่า "lvalue" เป็นเอนทิตีที่กำหนดได้ (ส่วนด้านซ้ายของ=
โอเปอเรเตอร์) ดังนั้นคุณต้องมีการอ้างอิงแบบมีชื่อกับวัตถุเพื่อให้สามารถทำหน้าที่เป็น lvalue ได้ rvalue ตรงข้ามกับวัตถุที่ไม่มีชื่ออ้างอิง ด้วยเหตุนี้วัตถุที่ไม่ระบุชื่อและ rvalue จึงเป็นคำพ้องความหมาย ดังนั้น:
class A
{
int i, *p;
public:
// Copy
A(const A& a) : i(a.i), p(new int(*a.p)) {}
// Movement (&& means "rvalue reference to")
A(A&& a) : i(a.i), p(a.p)
{
a.p = nullptr;
}
~A() { delete p; }
};
ในกรณีนี้เมื่อวัตถุประเภทA
ควร "คัดลอก" คอมไพเลอร์สร้างการอ้างอิง lvalue หรือการอ้างอิง rvalue ตามว่าวัตถุที่ผ่านการตั้งชื่อหรือไม่ เมื่อไม่ใช้งานตัวสร้างการเคลื่อนไหวของคุณจะถูกเรียกใช้และคุณรู้ว่าวัตถุนั้นเป็นเวลาชั่วคราวและคุณสามารถย้ายวัตถุแบบไดนามิกได้แทนที่จะคัดลอกสิ่งเหล่านั้นประหยัดพื้นที่และหน่วยความจำ
เป็นสิ่งสำคัญที่ต้องจำไว้ว่าวัตถุ "คงที่" จะถูกคัดลอกเสมอ ไม่มีวิธี "ย้าย" วัตถุคงที่ (วัตถุในกองซ้อนและไม่อยู่บนกอง) ดังนั้นความแตกต่าง "ย้าย" / "คัดลอก" เมื่อวัตถุไม่มีสมาชิกแบบไดนามิก (โดยตรงหรือโดยอ้อม) ไม่เกี่ยวข้อง
หากวัตถุของคุณมีความซับซ้อนและผู้ทำลายล้างมีเอฟเฟกต์รองอื่น ๆ เช่นการเรียกไปยังฟังก์ชั่นของห้องสมุดการโทรไปยังฟังก์ชั่นอื่น ๆ ของโลกหรือสิ่งที่มันอาจจะเป็นการดีกว่าที่จะส่งสัญญาณการเคลื่อนไหว
class Heavy
{
bool b_moved;
// staff
public:
A(const A& a) { /* definition */ }
A(A&& a) : // initialization list
{
a.b_moved = true;
}
~A() { if (!b_moved) /* destruct object */ }
};
ดังนั้นรหัสของคุณจะสั้นกว่า (คุณไม่จำเป็นต้องทำการnullptr
มอบหมายสำหรับสมาชิกไดนามิกแต่ละราย) และทั่วไปมากขึ้น
คำถามทั่วไปอื่น ๆ : อะไรคือความแตกต่างระหว่างA&&
และconst A&&
? แน่นอนในกรณีแรกคุณสามารถปรับเปลี่ยนวัตถุและในครั้งที่สองไม่ได้ แต่ความหมายในทางปฏิบัติ? ในกรณีที่สองคุณไม่สามารถแก้ไขได้ดังนั้นคุณจึงไม่มีวิธีที่จะทำให้วัตถุเป็นโมฆะ (ยกเว้นธงที่ไม่แน่นอนหรืออะไรทำนองนั้น) และไม่มีความแตกต่างในทางปฏิบัติกับตัวสร้างสำเนา
และการส่งต่อที่สมบูรณ์แบบคืออะไร? สิ่งสำคัญคือต้องรู้ว่า "การอ้างอิง rvalue" เป็นการอ้างอิงไปยังวัตถุที่มีชื่อใน "ขอบเขตของผู้โทร" แต่ในขอบเขตที่แท้จริงการอ้างอิง rvalue เป็นชื่อของวัตถุดังนั้นจึงทำหน้าที่เป็นวัตถุที่มีชื่อ หากคุณผ่านการอ้างอิง rvalue ไปยังฟังก์ชั่นอื่นคุณจะผ่านวัตถุที่มีชื่อดังนั้นวัตถุจะไม่ได้รับเช่นวัตถุทางโลก
void some_function(A&& a)
{
other_function(a);
}
วัตถุที่จะถูกคัดลอกไปพารามิเตอร์ที่แท้จริงของa
other_function
หากคุณต้องการให้วัตถุนั้นa
ยังคงถูกใช้เป็นวัตถุชั่วคราวต่อไปคุณควรใช้std::move
ฟังก์ชัน:
other_function(std::move(a));
ด้วยบรรทัดนี้std::move
จะส่งa
ไปยังค่า rvalue และother_function
จะได้รับวัตถุเป็นวัตถุที่ไม่มีชื่อ แน่นอนถ้าother_function
ไม่มีการบรรทุกเกินพิกัดที่เฉพาะเจาะจงในการทำงานกับวัตถุที่ไม่มีชื่อความแตกต่างนี้ไม่สำคัญ
การส่งต่อที่สมบูรณ์แบบนั้นคืออะไร? ไม่ แต่เราสนิทกันมาก การส่งต่อที่สมบูรณ์แบบนั้นมีประโยชน์สำหรับการทำงานกับเทมเพลตโดยมีวัตถุประสงค์เพื่อกล่าวว่า: หากฉันต้องการส่งวัตถุไปยังฟังก์ชันอื่นฉันต้องใช้ว่าถ้าฉันได้รับวัตถุที่มีชื่อวัตถุจะถูกส่งเป็นวัตถุที่มีชื่อและเมื่อไม่ ฉันต้องการผ่านมันเหมือนวัตถุที่ไม่มีชื่อ:
template<typename T>
void some_function(T&& a)
{
other_function(std::forward<T>(a));
}
นั่นคือลายเซ็นของฟังก์ชั่นที่ใช้แม่บทการส่งต่อที่สมบูรณ์แบบนำมาใช้ใน C ++ 11 std::forward
โดยวิธีการของ ฟังก์ชันนี้ใช้ประโยชน์จากกฎของการสร้างอินสแตนซ์ของเทมเพลต:
`A& && == A&`
`A&& && == A&&`
ดังนั้นหากT
การอ้างอิง lvalue ไปที่A
( T = A &) a
ก็เช่นกัน ( A &&& => A &) หากT
มีการอ้างอิง rvalue ไปA
, a
ยัง (A && && => เอ &&) ในทั้งสองกรณีa
เป็นวัตถุที่มีชื่อในขอบเขตที่แท้จริง แต่T
มีข้อมูลของ "ประเภทอ้างอิง" ของมันจากมุมมองของขอบเขตผู้โทร ข้อมูลนี้ ( T
) จะถูกส่งเป็นพารามิเตอร์แม่แบบforward
และ '' T
จะย้ายหรือไม่เป็นไปตามประเภทของ
มันเหมือนกับความหมายของการคัดลอก แต่แทนที่จะต้องทำซ้ำข้อมูลทั้งหมดที่คุณได้รับเพื่อขโมยข้อมูลจากวัตถุที่ถูก "ย้าย" จาก
คุณรู้ไหมว่าความหมายของการทำสำเนาหมายถึงอะไร? มันหมายความว่าคุณมีประเภทที่สามารถคัดลอกได้สำหรับประเภทที่ผู้ใช้กำหนดเองคุณกำหนดสิ่งนี้อย่างใดอย่างหนึ่งซื้อการเขียนตัวสร้างสำเนา & ตัวดำเนินการกำหนดหรือคอมไพเลอร์สร้างพวกเขาโดยปริยาย จะทำสำเนา
ความหมายของการย้ายนั้นเป็นประเภทที่ผู้ใช้กำหนดเองพร้อมตัวสร้างที่ใช้การอ้างอิง r-value (การอ้างอิงชนิดใหม่โดยใช้ && (ใช่แอมป์สองตัว)) ซึ่งไม่ใช่แบบ const นี่เรียกว่าตัวสร้างการย้าย ตัวสร้างการย้ายทำอะไรได้ดีแทนที่จะคัดลอกหน่วยความจำจากอาร์กิวเมนต์แหล่งที่มามัน 'ย้าย' หน่วยความจำจากแหล่งไปยังปลายทาง
คุณต้องการทำเช่นนั้นเมื่อใด well std :: vector เป็นตัวอย่างสมมติว่าคุณสร้าง std ชั่วคราว :: vector และคุณส่งคืนจากฟังก์ชันบอกว่า:
std::vector<foo> get_foos();
คุณจะมีค่าใช้จ่ายจากตัวสร้างการคัดลอกเมื่อฟังก์ชันส่งคืนถ้า (และจะเป็น C ++ 0x) std :: vector มีตัวสร้างการย้ายแทนที่จะคัดลอกมันสามารถตั้งค่าตัวชี้และ 'ย้าย' การจัดสรรแบบไดนามิก หน่วยความจำในอินสแตนซ์ใหม่ มันเหมือนกับซีแมนทิกส์การโอนสิทธิ์การเป็นเจ้าของด้วย std :: auto_ptr
เพื่อแสดงความต้องการย้ายความหมายลองพิจารณาตัวอย่างนี้โดยไม่ต้องมีความหมายย้าย:
นี่คือฟังก์ชั่นที่รับอ็อบเจกต์ประเภทT
และส่งคืนออบเจ็กต์ประเภทเดียวกันT
:
T f(T o) { return o; }
//^^^ new object constructed
ฟังก์ชั่นด้านบนใช้การเรียกตามค่าซึ่งหมายความว่าเมื่อฟังก์ชั่นนี้เรียกว่าวัตถุจะต้องสร้างขึ้นเพื่อใช้งานโดยฟังก์ชั่น
เนื่องจากฟังก์ชั่นส่งคืนตามมูลค่าวัตถุใหม่อีกรายการหนึ่งถูกสร้างขึ้นสำหรับค่าส่งคืน:
T b = f(a);
//^ new object constructed
มีการสร้างวัตถุใหม่สองรายการหนึ่งในนั้นเป็นวัตถุชั่วคราวที่ใช้สำหรับช่วงเวลาของฟังก์ชันเท่านั้น
เมื่อมีการสร้างวัตถุใหม่จากค่าส่งคืนตัวสร้างการคัดลอกจะถูกเรียกให้คัดลอกเนื้อหาของวัตถุชั่วคราวไปยังวัตถุใหม่ข หลังจากที่ฟังก์ชั่นเสร็จสมบูรณ์วัตถุชั่วคราวที่ใช้ในฟังก์ชั่นจะออกจากขอบเขตและถูกทำลาย
ทีนี้ลองมาดูกันว่าตัวสร้างสำเนาทำอะไร
ก่อนอื่นจะต้องเริ่มต้นวัตถุจากนั้นคัดลอกข้อมูลที่เกี่ยวข้องทั้งหมดจากวัตถุเก่าไปยังวัตถุใหม่
ขึ้นอยู่กับคลาสอาจเป็นคอนเทนเนอร์ที่มีข้อมูลจำนวนมากจากนั้นอาจแสดงเวลาและการใช้หน่วยความจำได้มาก
// Copy constructor
T::T(T &old) {
copy_data(m_a, old.m_a);
copy_data(m_b, old.m_b);
copy_data(m_c, old.m_c);
}
ด้วยซีแมนทิกส์ย้ายตอนนี้เป็นไปได้ที่จะทำให้งานนี้ได้ผลเสียน้อยที่สุดเพียงแค่ย้ายข้อมูลแทนที่จะคัดลอก
// Move constructor
T::T(T &&old) noexcept {
m_a = std::move(old.m_a);
m_b = std::move(old.m_b);
m_c = std::move(old.m_c);
}
การย้ายข้อมูลเกี่ยวข้องกับการเชื่อมโยงข้อมูลกับวัตถุใหม่ และไม่มีการคัดลอกเกิดขึ้นเลย
นี่คือความสำเร็จที่มีการrvalue
อ้างอิง
การrvalue
อ้างอิงทำงานคล้ายกับการlvalue
อ้างอิงที่มีความแตกต่างที่สำคัญอย่างหนึ่ง:
การอ้างอิง rvalue สามารถเคลื่อนย้ายได้และlvalueไม่สามารถทำได้
จากcppreference.com :
เพื่อให้การรับประกันข้อยกเว้นที่แข็งแกร่งเป็นไปได้ผู้สร้างการย้ายที่ผู้ใช้กำหนดเองไม่ควรโยนข้อยกเว้น ในความเป็นจริงคอนเทนเนอร์มาตรฐานมักใช้ std :: move_if_noexcept เพื่อเลือกระหว่างการย้ายและการคัดลอกเมื่อต้องการย้ายองค์ประกอบของคอนเทนเนอร์ หากทั้งตัวคัดลอกและตัวย้ายถูกจัดเตรียมความละเอียดเกินพิกัดจะเลือกตัวสร้างการย้ายหากอาร์กิวเมนต์เป็น rvalue (เช่น prvalue เช่นชั่วคราวแบบไม่มีชื่อหรือ xvalue เช่นผลลัพธ์ของ std :: move) และเลือกตัวสร้างสำเนาหาก อาร์กิวเมนต์เป็น lvalue (ชื่อวัตถุหรือฟังก์ชั่น / ผู้ประกอบการกลับมาอ้างอิง lvalue) หากมีเพียงตัวสร้างการคัดลอกไว้ให้หมวดหมู่อาร์กิวเมนต์ทั้งหมดเลือก (ตราบเท่าที่ใช้การอ้างอิงถึง const เนื่องจากค่า rvalues สามารถผูกกับการอ้างอิง const) ซึ่งทำให้การคัดลอกทางเลือกสำหรับการย้ายเมื่อการย้ายไม่พร้อมใช้งาน ในหลาย ๆ สถานการณ์ตัวสร้างการเคลื่อนย้ายได้รับการปรับให้เหมาะสมแม้ว่าพวกเขาจะสร้างผลข้างเคียงที่สังเกตได้ดูการคัดลอก คอนสตรัคถูกเรียกว่า 'ย้ายคอนสตรัคเตอร์' เมื่อใช้การอ้างอิง rvalue เป็นพารามิเตอร์ ไม่จำเป็นต้องย้ายสิ่งใด ๆ คลาสไม่จำเป็นต้องมีทรัพยากรที่จะย้ายและ 'ตัวสร้างการย้าย' อาจไม่สามารถย้ายทรัพยากรได้ในกรณีที่อนุญาต (แต่อาจจะไม่สมเหตุสมผล) ที่พารามิเตอร์เป็น const rvalue การอ้างอิง (const T&&)
ฉันกำลังเขียนสิ่งนี้เพื่อให้แน่ใจว่าฉันเข้าใจถูกต้อง
ซีแมนทิกส์ย้ายถูกสร้างขึ้นเพื่อหลีกเลี่ยงการคัดลอกวัตถุขนาดใหญ่โดยไม่จำเป็น Bjarne Stroustrup ในหนังสือของเขา "ภาษาการเขียนโปรแกรม C ++" ใช้สองตัวอย่างที่การคัดลอกที่ไม่จำเป็นเกิดขึ้นโดยค่าเริ่มต้น: หนึ่งการแลกเปลี่ยนสองวัตถุขนาดใหญ่และสองการคืนค่าของวัตถุขนาดใหญ่จากวิธีการ
การสลับสองวัตถุขนาดใหญ่มักเกี่ยวข้องกับการคัดลอกวัตถุแรกไปยังวัตถุชั่วคราวคัดลอกวัตถุที่สองไปยังวัตถุแรกและคัดลอกวัตถุชั่วคราวไปยังวัตถุที่สอง สำหรับประเภทในตัวสิ่งนี้เร็วมาก แต่สำหรับวัตถุขนาดใหญ่ทั้งสามชุดนี้อาจใช้เวลานาน "การกำหนดย้าย" ช่วยให้โปรแกรมเมอร์สามารถแทนที่พฤติกรรมการคัดลอกเริ่มต้นและแทนที่จะสลับการอ้างอิงไปยังวัตถุซึ่งหมายความว่าไม่มีการคัดลอกเลยและการดำเนินการสลับนั้นเร็วกว่ามาก การมอบหมายการย้ายสามารถถูกเรียกใช้โดยเรียกเมธอด std :: move ()
การส่งคืนวัตถุจากวิธีการโดยค่าเริ่มต้นเกี่ยวข้องกับการทำสำเนาของวัตถุในท้องถิ่นและข้อมูลที่เกี่ยวข้องในสถานที่ซึ่งผู้โทรสามารถเข้าถึงได้ (เนื่องจากวัตถุในเครื่องไม่สามารถเข้าถึงผู้โทรได้และหายไปเมื่อวิธีการเสร็จสิ้น) เมื่อมีการส่งคืนชนิดในตัวการดำเนินการนี้จะเร็วมาก แต่ถ้ามีการส่งคืนวัตถุขนาดใหญ่อาจใช้เวลานาน ตัวสร้างการย้ายอนุญาตให้โปรแกรมเมอร์เขียนทับลักษณะการทำงานเริ่มต้นนี้และ "นำ" ข้อมูลฮีพที่เกี่ยวข้องกับวัตถุในท้องถิ่นกลับมาใช้ใหม่โดยการชี้วัตถุที่ถูกส่งคืนไปยังผู้โทรเพื่อกองข้อมูลที่เกี่ยวข้องกับวัตถุในท้องถิ่น ดังนั้นจึงไม่จำเป็นต้องคัดลอก
ในภาษาที่ไม่อนุญาตให้สร้างวัตถุท้องถิ่น (นั่นคือวัตถุในสแต็ค) ปัญหาประเภทนี้ไม่เกิดขึ้นเนื่องจากวัตถุทั้งหมดได้รับการจัดสรรบนฮีปและเข้าถึงได้โดยอ้างอิงเสมอ
x
และy
คุณไม่สามารถเพียง"สลับการอ้างอิงไปยังวัตถุ" ; อาจเป็นไปได้ว่าวัตถุนั้นมีพอยน์เตอร์ที่อ้างอิงข้อมูลอื่นและพอยน์เตอร์เหล่านั้นสามารถสลับได้ แต่ตัวดำเนินการย้ายไม่จำเป็นต้องสลับอะไร พวกเขาอาจล้างข้อมูลจากวัตถุที่ย้ายจากแทนที่จะเก็บรักษาข้อมูลปลายทางในนั้น
swap()
โดยไม่ต้องมีความหมายย้าย "การมอบหมายการย้ายสามารถเรียกใช้โดยการเรียกวิธี std :: move ()" - บางครั้งจำเป็นต้องใช้std::move()
- แม้ว่ามันจะไม่ได้ย้ายอะไรเลย - เพียงแค่ให้คอมไพเลอร์รู้ว่าอาร์กิวเมนต์นั้นสามารถเคลื่อนย้ายได้บางครั้งstd::forward<>()
(พร้อมการอ้างอิงการส่งต่อ) และเวลาอื่น ๆ ที่คอมไพเลอร์รู้ค่าสามารถเคลื่อนย้ายได้
นี่คือคำตอบจากหนังสือ "ภาษาการเขียนโปรแกรม C ++" โดย Bjarne Stroustrup หากคุณไม่ต้องการดูวิดีโอคุณสามารถดูข้อความด้านล่าง:
พิจารณาตัวอย่างนี้ การกลับมาจากโอเปอเรเตอร์ + เกี่ยวข้องกับการคัดลอกผลลัพธ์จากตัวแปรโลคัลres
และในบางแห่งที่ผู้เรียกสามารถเข้าถึงได้
Vector operator+(const Vector& a, const Vector& b)
{
if (a.size()!=b.size())
throw Vector_siz e_mismatch{};
Vector res(a.size());
for (int i=0; i!=a.size(); ++i)
res[i]=a[i]+b[i];
return res;
}
เราไม่ต้องการสำเนาจริงๆ เราแค่อยากได้ผลลัพธ์จากฟังก์ชั่น ดังนั้นเราต้องย้ายเวกเตอร์แทนที่จะคัดลอก เราสามารถกำหนดตัวสร้างการย้ายดังนี้:
class Vector {
// ...
Vector(const Vector& a); // copy constructor
Vector& operator=(const Vector& a); // copy assignment
Vector(Vector&& a); // move constructor
Vector& operator=(Vector&& a); // move assignment
};
Vector::Vector(Vector&& a)
:elem{a.elem}, // "grab the elements" from a
sz{a.sz}
{
a.elem = nullptr; // now a has no elements
a.sz = 0;
}
&& หมายถึง "การอ้างอิง rvalue" และเป็นการอ้างอิงถึงการที่เราสามารถผูก rvalue ได้ "rvalue" 'มีวัตถุประสงค์เพื่อเสริม "lvalue" ซึ่งหมายถึง "สิ่งที่สามารถปรากฏทางด้านซ้ายมือของงานที่มอบหมาย" ดังนั้นค่า rvalue หมายถึง "ค่าที่คุณไม่สามารถกำหนดให้" อย่างเช่นจำนวนเต็มที่ส่งคืนโดยการเรียกใช้ฟังก์ชันและres
ตัวแปรโลคัลในตัวดำเนินการ + () สำหรับเวกเตอร์
ตอนนี้คำสั่งreturn res;
จะไม่คัดลอก!