คำถามที่สำคัญที่สุดอันดับแรก:
มีแนวทางปฏิบัติที่ดีที่สุดเกี่ยวกับวิธีการส่งพารามิเตอร์ใน C ++ หรือไม่เพราะฉันพบว่ามันไม่ใช่เรื่องเล็กน้อย
หากฟังก์ชันของคุณต้องการแก้ไขออบเจ็กต์ดั้งเดิมที่กำลังส่งผ่านดังนั้นหลังจากการโทรกลับมาแล้วผู้โทรจะมองเห็นการแก้ไขวัตถุนั้นคุณควรส่งผ่านการอ้างอิง lvalue :
void foo(my_class& obj)
{
// Modify obj here...
}
หากฟังก์ชันของคุณไม่จำเป็นต้องแก้ไขออบเจ็กต์ดั้งเดิมและไม่จำเป็นต้องสร้างสำเนาของมัน (กล่าวอีกนัยหนึ่งก็คือต้องสังเกตสถานะของมันเท่านั้น) คุณควรผ่านการอ้างอิง lvalue ไปที่const
:
void foo(my_class const& obj)
{
// Observe obj here
}
นี้จะช่วยให้คุณสามารถเรียกใช้ฟังก์ชันทั้งที่มี lvalues (lvalues เป็นวัตถุที่มีตัวตนที่มีเสถียรภาพ) และ rvalues (rvalues เป็นเช่นชั่วคราวหรือวัตถุที่คุณกำลังจะย้ายจากเป็นผลมาจากการเรียกstd::move()
)
หนึ่งยังได้ยืนยันว่าชนิดพื้นฐานหรือชนิดที่คัดลอกเป็นไปอย่างรวดเร็วเช่นint
, bool
หรือchar
ไม่มีความจำเป็นที่จะผ่านโดยการอ้างอิงถ้าฟังก์ชันก็ต้องสังเกตค่าและการส่งผ่านโดยค่าควรจะได้รับการสนับสนุน ถูกต้องหากอ้างอิงความหมายไม่จำเป็นต้องใช้แต่จะเกิดอะไรขึ้นถ้าฟังก์ชันต้องการจัดเก็บตัวชี้ไปยังวัตถุอินพุตเดียวกันนั้นไว้ที่ไหนสักแห่งเพื่อให้ในอนาคตอ่านผ่านตัวชี้นั้นจะเห็นการปรับเปลี่ยนค่าที่ดำเนินการในส่วนอื่น ๆ ของ รหัส? ในกรณีนี้การส่งผ่านข้อมูลอ้างอิงเป็นวิธีแก้ปัญหาที่ถูกต้อง
หากฟังก์ชันของคุณไม่จำเป็นต้องแก้ไขออบเจ็กต์ดั้งเดิม แต่จำเป็นต้องจัดเก็บสำเนาของวัตถุนั้น ( อาจส่งคืนผลลัพธ์ของการเปลี่ยนแปลงของอินพุตโดยไม่ต้องแก้ไขอินพุต ) คุณสามารถพิจารณาตามค่า :
void foo(my_class obj) // One copy or one move here, but not working on
// the original object...
{
// Working on obj...
// Possibly move from obj if the result has to be stored somewhere...
}
การเรียกใช้ฟังก์ชันข้างต้นจะส่งผลให้ได้สำเนาเดียวเสมอเมื่อส่งผ่านค่า lvalues และในการย้ายครั้งเดียวเมื่อส่งค่า rvalues หากฟังก์ชันของคุณต้องการจัดเก็บอ็อบเจ็กต์นี้ไว้ที่ใดที่หนึ่งคุณสามารถดำเนินการย้ายเพิ่มเติมจากอ็อบเจ็กต์ได้ (เช่นในกรณีfoo()
นี้เป็นฟังก์ชันสมาชิกที่ต้องการเก็บค่าในสมาชิกข้อมูล )
ในกรณีที่การเคลื่อนย้ายมีราคาแพงสำหรับอ็อบเจ็กต์ประเภทmy_class
ต่างๆคุณอาจพิจารณาโอเวอร์โหลดfoo()
และระบุเวอร์ชันหนึ่งสำหรับ lvalues (ยอมรับการอ้างอิง lvalue ถึงconst
) และเวอร์ชันหนึ่งสำหรับ rvalues (ยอมรับการอ้างอิง rvalue):
// Overload for lvalues
void foo(my_class const& obj) // No copy, no move (just reference binding)
{
my_class copyOfObj = obj; // Copy!
// Working on copyOfObj...
}
// Overload for rvalues
void foo(my_class&& obj) // No copy, no move (just reference binding)
{
my_class copyOfObj = std::move(obj); // Move!
// Notice, that invoking std::move() is
// necessary here, because obj is an
// *lvalue*, even though its type is
// "rvalue reference to my_class".
// Working on copyOfObj...
}
ฟังก์ชั่นข้างต้นมีความคล้ายคลึงกันมากจนคุณสามารถสร้างฟังก์ชันเดียวจากฟังก์ชันนี้ได้: foo()
อาจกลายเป็นเทมเพลตฟังก์ชันและคุณสามารถใช้การส่งต่อที่สมบูรณ์แบบเพื่อพิจารณาว่าการย้ายหรือสำเนาของวัตถุที่ส่งผ่านจะถูกสร้างขึ้นภายในหรือไม่:
template<typename C>
void foo(C&& obj) // No copy, no move (just reference binding)
// ^^^
// Beware, this is not always an rvalue reference! This will "magically"
// resolve into my_class& if an lvalue is passed, and my_class&& if an
// rvalue is passed
{
my_class copyOfObj = std::forward<C>(obj); // Copy if lvalue, move if rvalue
// Working on copyOfObj...
}
คุณอาจต้องการเรียนรู้เพิ่มเติมเกี่ยวกับการออกแบบนี้โดยดูคำบรรยายนี้ของ Scott Meyers (โปรดทราบว่าคำว่า " Universal References " ที่เขาใช้นั้นไม่เป็นมาตรฐาน)
สิ่งหนึ่งที่ควรทราบก็คือstd::forward
มักจะจบลงด้วยการย้ายค่า rvalues ดังนั้นแม้ว่ามันจะดูไร้เดียงสา แต่การส่งต่อวัตถุเดียวกันหลาย ๆ ครั้งอาจเป็นสาเหตุของปัญหาตัวอย่างเช่นการย้ายจากวัตถุเดียวกันสองครั้ง! ดังนั้นระวังอย่าใส่สิ่งนี้ในการวนซ้ำและอย่าส่งต่ออาร์กิวเมนต์เดียวกันหลาย ๆ ครั้งในการเรียกใช้ฟังก์ชัน:
template<typename C>
void foo(C&& obj)
{
bar(std::forward<C>(obj), std::forward<C>(obj)); // Dangerous!
}
โปรดสังเกตด้วยว่าโดยปกติคุณจะไม่หันไปใช้โซลูชันที่ใช้เทมเพลตเว้นแต่คุณจะมีเหตุผลที่ดีเนื่องจากจะทำให้โค้ดของคุณอ่านยากขึ้น ปกติคุณควรมุ่งเน้นความคมชัดและความเรียบง่าย
ข้างต้นเป็นเพียงแนวทางง่ายๆ แต่ส่วนใหญ่แล้วจะชี้ให้คุณตัดสินใจออกแบบที่ดี
คำนึงถึงส่วนที่เหลือของโพสต์ของคุณ:
ถ้าฉันเขียนใหม่เป็น [... ] จะมี 2 ท่าและไม่มีการคัดลอก
สิ่งนี้ไม่ถูกต้อง เริ่มต้นด้วยการอ้างอิง rvalue ไม่สามารถผูกกับ lvalue ได้ดังนั้นสิ่งนี้จะคอมไพล์ก็ต่อเมื่อคุณส่งค่า rvalue ของ type CreditCard
ไปยังตัวสร้างของคุณ ตัวอย่างเช่น:
// Here you are passing a temporary (OK! temporaries are rvalues)
Account acc("asdasd",345, CreditCard("12345",2,2015,1001));
CreditCard cc("12345",2,2015,1001);
// Here you are passing the result of std::move (OK! that's also an rvalue)
Account acc("asdasd",345, std::move(cc));
แต่จะไม่ได้ผลถ้าคุณพยายามทำสิ่งนี้:
CreditCard cc("12345",2,2015,1001);
Account acc("asdasd",345, cc); // ERROR! cc is an lvalue
เนื่องจากcc
เป็นการอ้างอิง lvalue และ rvalue ไม่สามารถผูกกับ lvalues ได้ ยิ่งไปกว่านั้นเมื่อเชื่อมโยงการอ้างอิงกับวัตถุจะไม่มีการย้ายใด ๆแต่เป็นเพียงการเชื่อมโยงข้อมูลอ้างอิง ดังนั้นจะมีเพียงหนึ่งย้าย
ดังนั้นตามแนวทางที่ให้ไว้ในส่วนแรกของคำตอบนี้หากคุณกังวลเกี่ยวกับจำนวนการเคลื่อนไหวที่สร้างขึ้นเมื่อคุณใช้ a CreditCard
by value คุณสามารถกำหนด constructor overloads ได้สองตัวโดยตัวหนึ่งใช้การอ้างอิง lvalue ไปที่const
( CreditCard const&
) และหนึ่งการใช้ การอ้างอิง rvalue ( CreditCard&&
)
ความละเอียดเกินจะเลือกอดีตเมื่อผ่าน lvalue (ในกรณีนี้จะดำเนินการหนึ่งสำเนา) และหลังเมื่อผ่านค่า rvalue (ในกรณีนี้จะทำการย้ายครั้งเดียว)
Account(std::string number, float amount, CreditCard const& creditCard)
: number(number), amount(amount), creditCard(creditCard) // copy here
{ }
Account(std::string number, float amount, CreditCard&& creditCard)
: number(number), amount(amount), creditCard(std::move(creditCard)) // move here
{ }
การใช้งานของคุณstd::forward<>
จะเห็นได้ตามปกติเมื่อคุณต้องการที่จะบรรลุการส่งต่อที่สมบูรณ์แบบ ในกรณีนั้นตัวสร้างของคุณจะเป็นเทมเพลตตัวสร้างและจะมีลักษณะมากหรือน้อยดังนี้
template<typename C>
Account(std::string number, float amount, C&& creditCard)
: number(number), amount(amount), creditCard(std::forward<C>(creditCard)) { }
ในแง่หนึ่งสิ่งนี้รวมทั้งโอเวอร์โหลดที่ฉันเคยแสดงไว้ก่อนหน้านี้เป็นฟังก์ชันเดียว: C
จะอนุมานได้CreditCard&
ในกรณีที่คุณส่งค่า lvalue และเนื่องจากกฎการยุบการอ้างอิงจะทำให้ฟังก์ชันนี้ถูกสร้างอินสแตนซ์:
Account(std::string number, float amount, CreditCard& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard&>(creditCard))
{ }
นี้จะทำให้เกิดการคัดลอกก่อสร้างของcreditCard
ที่คุณจะต้องการ ในทางกลับกันเมื่อค่า r ถูกส่งผ่านC
จะถูกอนุมานให้เป็นCreditCard
และฟังก์ชันนี้จะถูกสร้างอินสแตนซ์แทน:
Account(std::string number, float amount, CreditCard&& creditCard) :
number(num), amount(amount), creditCard(std::forward<CreditCard>(creditCard))
{ }
นี้จะทำให้เกิดการย้ายการก่อสร้างของcreditCard
ซึ่งเป็นสิ่งที่คุณต้องการ (เพราะค่าที่ถูกส่งเป็น rvalue และนั่นหมายความว่าเราจะได้รับอนุญาตให้ย้ายจากมัน)