เพื่อให้เข้าใจว่าเหตุใดจึงเป็นรูปแบบที่ดีเราควรตรวจสอบทางเลือกทั้งใน C ++ 03 และใน C ++ 11
เรามีวิธีการ C ++ 03 ในการstd::string const&
:
struct S
{
std::string data;
S(std::string const& str) : data(str)
{}
};
ในกรณีนี้จะมีเสมอจะเป็นสำเนาเดียวดำเนินการ หากคุณสร้างจากสตริง C ดิบ a std::string
จะถูกสร้างจากนั้นคัดลอกอีกครั้ง: สองการปันส่วน
มีวิธี C ++ 03 ในการอ้างอิงถึง a std::string
จากนั้นเปลี่ยนเป็นโลคัลstd::string
:
struct S
{
std::string data;
S(std::string& str)
{
std::swap(data, str);
}
};
นั่นคือ "move semantics" เวอร์ชัน C ++ 03 และswap
มักจะได้รับการปรับให้มีราคาถูกมาก (เหมือน a move
) นอกจากนี้ควรวิเคราะห์ในบริบท:
S tmp("foo");
std::string s("foo");
S tmp2(s);
และบังคับให้คุณสร้างแบบไม่ชั่วคราวstd::string
จากนั้นทิ้งไป (ชั่วคราวstd::string
ไม่สามารถผูกกับการอ้างอิงที่ไม่ใช่ const) อย่างไรก็ตามการจัดสรรเพียงรายการเดียวเท่านั้น เวอร์ชัน C ++ 11 จะใช้เวลา&&
และกำหนดให้คุณต้องเรียกใช้ด้วยstd::move
หรือชั่วคราว: สิ่งนี้ต้องการให้ผู้โทรสร้างสำเนานอกการโทรอย่างชัดเจนและย้ายสำเนานั้นไปยังฟังก์ชันหรือตัวสร้าง
struct S
{
std::string data;
S(std::string&& str): data(std::move(str))
{}
};
ใช้:
S tmp("foo");
std::string s("foo");
S tmp2(std::move(s));
ต่อไปเราสามารถทำ C ++ 11 เวอร์ชันเต็มซึ่งรองรับทั้งการคัดลอกและmove
:
struct S
{
std::string data;
S(std::string const& str) : data(str) {}
S(std::string && str) : data(std::move(str)) {}
};
จากนั้นเราสามารถตรวจสอบวิธีใช้:
S tmp( "foo" );
std::string bar("bar");
S tmp2( bar );
std::string bar2("bar2");
S tmp3( std::move(bar2) );
ค่อนข้างชัดเจนว่าเทคนิคโอเวอร์โหลด 2 อย่างนี้มีประสิทธิภาพอย่างน้อยที่สุดถ้าไม่มากไปกว่า C ++ 03 สองสไตล์ข้างต้น ฉันจะพากย์ 2-overload เวอร์ชันนี้เป็นเวอร์ชันที่ "เหมาะสมที่สุด"
ตอนนี้เราจะตรวจสอบเวอร์ชันถ่ายโดยสำเนา:
struct S2 {
std::string data;
S2( std::string arg ):data(std::move(x)) {}
};
ในแต่ละสถานการณ์เหล่านั้น:
S2 tmp( "foo" );
std::string bar("bar");
S2 tmp2( bar );
std::string bar2("bar2");
S2 tmp3( std::move(bar2) );
หากคุณเปรียบเทียบสิ่งนี้แบบเคียงข้างกันกับเวอร์ชันที่ "เหมาะสมที่สุด" เราจะทำเพิ่มเติมอีกหนึ่งข้อmove
! เราไม่ได้ทำพิเศษเพียงครั้งเดียวcopy
ไม่ใช่แค่ครั้งเดียวเราจะทำพิเศษ
ดังนั้นถ้าเราสมมติว่า move
ราคาถูกเวอร์ชันนี้จะทำให้เรามีประสิทธิภาพใกล้เคียงกับเวอร์ชันที่เหมาะสมที่สุด แต่มีโค้ดน้อยกว่า 2 เท่า
และถ้าคุณกำลังพูด 2 ถึง 10 อาร์กิวเมนต์การลดโค้ดจะเป็นเลขชี้กำลัง - น้อยกว่า 2 เท่าโดยมี 1 อาร์กิวเมนต์ 4x กับ 2, 8x กับ 3, 16x กับ 4, 1024x พร้อมด้วยอาร์กิวเมนต์ 10
ตอนนี้เราสามารถแก้ไขปัญหานี้ได้ผ่านการส่งต่อที่สมบูรณ์แบบและ SFINAE ช่วยให้คุณสามารถเขียนตัวสร้างหรือเทมเพลตฟังก์ชันเดียวที่รับอาร์กิวเมนต์ 10 อาร์กิวเมนต์ SFINAE เพื่อให้แน่ใจว่าอาร์กิวเมนต์เป็นประเภทที่เหมาะสมจากนั้นย้ายหรือคัดลอกไปยัง รัฐท้องถิ่นตามความต้องการ แม้ว่าสิ่งนี้จะป้องกันปัญหาขนาดโปรแกรมเพิ่มขึ้นเป็นพันเท่า แต่ก็ยังมีฟังก์ชันมากมายที่สร้างขึ้นจากเทมเพลตนี้ (อินสแตนซ์ฟังก์ชันเทมเพลตสร้างฟังก์ชัน)
และฟังก์ชันที่สร้างขึ้นจำนวนมากหมายถึงขนาดโค้ดที่ปฏิบัติการได้ใหญ่ขึ้นซึ่งสามารถลดประสิทธิภาพได้
สำหรับค่าใช้จ่ายไม่กี่ move
วินาทีเราได้รับโค้ดที่สั้นลงและประสิทธิภาพใกล้เคียงกันและมักจะเข้าใจรหัสได้ง่ายขึ้น
ตอนนี้สิ่งนี้ใช้ได้ผลเพราะเรารู้ว่าเมื่อมีการเรียกใช้ฟังก์ชัน (ในกรณีนี้คือตัวสร้าง) เราจะต้องการสำเนาเฉพาะของอาร์กิวเมนต์นั้น แนวคิดก็คือถ้าเรารู้ว่าเรากำลังจะทำสำเนาเราควรแจ้งให้ผู้โทรทราบว่าเรากำลังทำสำเนาโดยใส่ไว้ในรายการอาร์กิวเมนต์ของเรา จากนั้นพวกเขาสามารถปรับให้เหมาะสมกับข้อเท็จจริงที่ว่าพวกเขากำลังจะให้สำเนาแก่เรา (โดยย้ายไปที่อาร์กิวเมนต์ของเราเป็นต้น)
ข้อดีอีกประการหนึ่งของเทคนิค "take by value" คือบ่อยครั้งที่การย้ายตัวสร้างนั้นไม่มีข้อยกเว้นนั่นหมายความว่าฟังก์ชันที่รับค่าตามค่าและย้ายออกจากอาร์กิวเมนต์มักจะไม่มีข้อยกเว้นการย้ายthrow
s ใด ๆออกจากร่างกายและไปยังขอบเขตการเรียก (ผู้ที่สามารถหลีกเลี่ยงได้ผ่านการก่อสร้างโดยตรงในบางครั้งหรือสร้างสิ่งของและmove
เข้าสู่การโต้แย้งเพื่อควบคุมว่าจะเกิดการขว้างปาที่ใด) การสร้างวิธีการไม่โยนมักจะคุ้มค่า