มันจะดีกว่าใน C ++ ที่จะผ่านค่าหรือผ่านการอ้างอิงคงที่?
ฉันสงสัยว่าวิธีไหนดีกว่ากัน ฉันรู้ว่าการส่งผ่านโดยการอ้างอิงอย่างต่อเนื่องควรให้ประสิทธิภาพที่ดีขึ้นในโปรแกรมเพราะคุณไม่ได้ทำสำเนาของตัวแปร
มันจะดีกว่าใน C ++ ที่จะผ่านค่าหรือผ่านการอ้างอิงคงที่?
ฉันสงสัยว่าวิธีไหนดีกว่ากัน ฉันรู้ว่าการส่งผ่านโดยการอ้างอิงอย่างต่อเนื่องควรให้ประสิทธิภาพที่ดีขึ้นในโปรแกรมเพราะคุณไม่ได้ทำสำเนาของตัวแปร
คำตอบ:
มันเคยได้รับการแนะนำโดยทั่วไปปฏิบัติที่ดีที่สุด1ในการใช้งานผ่านโดนโทษ const สำหรับทุกประเภทยกเว้นประเภท builtin ( char
, int
, double
ฯลฯ ) สำหรับ iterators และฟังก์ชั่นสำหรับวัตถุ (lambdas เรียนมาจากstd::*_function
)
นี่คือความจริงโดยเฉพาะอย่างยิ่งก่อนการดำรงอยู่ของความหมายย้าย เหตุผลนั้นง่าย: ถ้าคุณส่งผ่านค่าจะต้องทำสำเนาของวัตถุและยกเว้นวัตถุขนาดเล็กมากสิ่งนี้จะมีราคาแพงกว่าการส่งผ่านการอ้างอิงเสมอ
ด้วย C ++ 11 เราได้รับความหมายย้าย สรุปย้ายความหมายอนุญาตให้ในบางกรณีวัตถุสามารถส่งผ่าน "ตามค่า" โดยไม่ต้องคัดลอก โดยเฉพาะอย่างยิ่งเป็นกรณีนี้เมื่อวัตถุที่คุณจะผ่านเป็นrvalue
ในตัวมันเองการเคลื่อนย้ายวัตถุก็ยังมีค่าใช้จ่ายอย่างน้อยก็ผ่านการอ้างอิง อย่างไรก็ตามในหลายกรณีฟังก์ชั่นจะคัดลอกภายในวัตถุอยู่แล้ว - เช่นมันจะเป็นเจ้าของอาร์กิวเมนต์ 2
ในสถานการณ์เหล่านี้เรามีการแลกเปลี่ยน (ง่าย) ดังต่อไปนี้:
“ ผ่านค่า” ยังคงเป็นสาเหตุให้วัตถุถูกคัดลอกเว้นแต่ว่าวัตถุนั้นเป็นค่า rvalue ในกรณีของค่า rvalue วัตถุสามารถถูกย้ายแทนดังนั้นในกรณีที่สองไม่ได้“ คัดลอกแล้วย้าย” อีกต่อไปทันที แต่“ ย้ายจากนั้น (อาจ) ย้ายอีกครั้ง”
สำหรับวัตถุขนาดใหญ่ที่ใช้ก่อสร้างย้ายที่เหมาะสม (เช่นเวกเตอร์สตริง ... ), กรณีที่สองเป็นแล้วอย่างมากมายมีประสิทธิภาพมากขึ้นกว่าครั้งแรก ดังนั้นจึงขอแนะนำให้ใช้ผ่านค่าถ้าฟังก์ชั่นใช้เวลาความเป็นเจ้าของของการโต้แย้งและถ้าชนิดของวัตถุที่เคลื่อนไหวสนับสนุนที่มีประสิทธิภาพ
บันทึกประวัติศาสตร์:
ในความเป็นจริงคอมไพเลอร์ที่ทันสมัยควรจะสามารถคิดออกเมื่อผ่านตามตัวอักษรมีราคาแพงและแปลงโทรไปใช้อ้างอิงโดยนัยถ้าเป็นไปได้
ในทางทฤษฎี ในทางปฏิบัติคอมไพเลอร์ไม่สามารถเปลี่ยนแปลงสิ่งนี้ได้ตลอดเวลาโดยไม่ทำลายส่วนต่อประสานไบนารีของฟังก์ชัน ในบางกรณีพิเศษ (เมื่อฟังก์ชั่นอินไลน์) สำเนาจะถูกนำออกมาจริง ๆ หากคอมไพเลอร์สามารถเข้าใจได้ว่าวัตถุดั้งเดิมจะไม่ถูกเปลี่ยนผ่านการดำเนินการในฟังก์ชั่น
แต่โดยทั่วไปแล้วคอมไพเลอร์ไม่สามารถระบุสิ่งนี้ได้และการกำเนิดความหมายของการย้ายใน C ++ ทำให้การเพิ่มประสิทธิภาพนี้มีความเกี่ยวข้องน้อยลง
1เช่นในสกอตต์เมเยอร์สที่มีประสิทธิภาพ C ++
2สิ่งนี้มักจะเป็นจริงสำหรับผู้สร้างวัตถุซึ่งอาจรับข้อโต้แย้งและเก็บไว้ภายในเพื่อเป็นส่วนหนึ่งของสถานะของวัตถุที่สร้างขึ้น
แก้ไข:บทความใหม่โดย Dave Abrahams ใน cpp-next:
การส่งผ่านค่าสำหรับ structs ที่การคัดลอกราคาถูกมีข้อดีเพิ่มเติมที่คอมไพเลอร์อาจคิดว่าวัตถุนั้นไม่ใช่นามแฝง (ไม่ใช่วัตถุเดียวกัน) การใช้ Pass-by-Reference คอมไพเลอร์ไม่สามารถคาดเดาได้เสมอ ตัวอย่างง่ายๆ:
foo * f;
void bar(foo g) {
g.i = 10;
f->i = 2;
g.i += 5;
}
คอมไพเลอร์สามารถเพิ่มประสิทธิภาพให้เป็น
g.i = 15;
f->i = 2;
เนื่องจากรู้ว่า f และ g ไม่ได้แชร์ตำแหน่งเดียวกัน ถ้า g เป็นข้อมูลอ้างอิง (foo &) คอมไพเลอร์ก็ไม่สามารถคาดเดาได้ เนื่องจาก gi สามารถเป็น aliased ได้โดย f-> i และต้องมีค่าเป็น 7 ดังนั้นคอมไพเลอร์จะต้องดึงค่าใหม่ของ gi จากหน่วยความจำอีกครั้ง
สำหรับกฎ pratical เพิ่มเติมนี่คือชุดของกฎที่ดีที่พบในบทความMove Constructors (ขอแนะนำให้อ่าน)
"ดั้งเดิม" ด้านบนหมายถึงชนิดข้อมูลขนาดเล็กโดยทั่วไปที่มีความยาวไม่กี่ไบต์และไม่ polymorphic (ตัววนซ้ำวัตถุฟังก์ชัน ฯลฯ ... ) หรือมีราคาแพงในการคัดลอก ในบทความนั้นมีกฎอีกข้อหนึ่ง แนวคิดคือบางครั้งเราต้องการทำสำเนา (ในกรณีที่ไม่สามารถแก้ไขข้อโต้แย้งได้) และบางครั้งก็ไม่ต้องการ (ในกรณีที่ต้องการใช้อาร์กิวเมนต์เองในฟังก์ชันหากอาร์กิวเมนต์เป็นแบบชั่วคราวอยู่แล้ว , ตัวอย่างเช่น). กระดาษอธิบายรายละเอียดวิธีการที่สามารถทำได้ ใน C ++ 1x เทคนิคนั้นสามารถนำมาใช้ได้กับการสนับสนุนทางภาษา ก่อนหน้านั้นฉันจะทำตามกฎข้างต้น
ตัวอย่าง: ในการสร้างสตริงตัวพิมพ์ใหญ่และส่งคืนเวอร์ชันตัวพิมพ์ใหญ่หนึ่งควรส่งค่าตามตัวอักษร: หนึ่งจะต้องคัดลอกมันต่อไป (หนึ่งไม่สามารถเปลี่ยนการอ้างอิง const โดยตรง) - ดีกว่าทำให้โปร่งใสที่สุด ผู้เรียกและทำสำเนานั้นก่อนเพื่อให้ผู้โทรสามารถปรับให้เหมาะสมที่สุดเท่าที่จะเป็นไปได้ - ดังรายละเอียดในกระดาษนั้น:
my::string uppercase(my::string s) { /* change s and return it */ }
อย่างไรก็ตามหากคุณไม่จำเป็นต้องเปลี่ยนพารามิเตอร์ให้ดำเนินการโดยอ้างอิงกับ const:
bool all_uppercase(my::string const& s) {
/* check to see whether any character is uppercase */
}
อย่างไรก็ตามหากคุณวัตถุประสงค์ของพารามิเตอร์คือการเขียนบางอย่างลงในอาร์กิวเมนต์ให้ส่งผ่านโดยการอ้างอิงที่ไม่ใช่แบบ const
bool try_parse(T text, my::string &out) {
/* try to parse, write result into out */
}
__restrict__
(ซึ่งสามารถใช้อ้างอิง) ได้มากกว่าทำสำเนามากเกินไป C ++ มาตรฐานที่แย่เกินไปไม่ได้ใช้restrict
คำสำคัญของ C99
ขึ้นอยู่กับประเภท คุณกำลังเพิ่มค่าใช้จ่ายเล็ก ๆ ที่ต้องทำการอ้างอิงและการอ้างอิง สำหรับประเภทที่มีขนาดเท่ากันหรือเล็กกว่าพอยน์เตอร์ที่ใช้ ctor คัดลอกเริ่มต้นมันอาจจะเร็วกว่าที่จะผ่านค่า
เมื่อมีการชี้ให้เห็นมันก็ขึ้นอยู่กับประเภท สำหรับชนิดข้อมูลในตัววิธีที่ดีที่สุดคือส่งผ่านค่า แม้แต่โครงสร้างที่มีขนาดเล็กมากเช่น ints คู่หนึ่งสามารถทำงานได้ดีขึ้นโดยการส่งผ่านค่า
นี่คือตัวอย่างสมมติว่าคุณมีค่าจำนวนเต็มและคุณต้องการส่งผ่านไปยังรูทีนอื่น หากค่านั้นได้รับการปรับให้เหมาะสมเพื่อเก็บไว้ในรีจิสเตอร์ถ้าคุณต้องการส่งผ่านการอ้างอิงก่อนอื่นจะต้องถูกเก็บไว้ในหน่วยความจำก่อนจากนั้นตัวชี้ไปยังหน่วยความจำนั้นบนสแต็กเพื่อทำการโทร ถ้ามันถูกส่งผ่านโดยค่าทั้งหมดที่จำเป็นต้องมีคือการลงทะเบียนผลักลงบนสแต็ก (รายละเอียดมีความซับซ้อนเล็กน้อยกว่าที่ได้รับจากระบบการโทรและ CPU ที่แตกต่างกัน)
หากคุณกำลังเขียนโปรแกรมแม่แบบคุณมักจะถูกบังคับให้ผ่านการอ้างอิง const เนื่องจากคุณไม่ทราบประเภทที่ถูกส่งเข้ามาการผ่านบทลงโทษสำหรับการส่งสิ่งที่ไม่ดีตามมูลค่านั้นแย่กว่าโทษของการผ่านประเภทในตัว โดยอ้างอิง const
นี่คือสิ่งที่ฉันทำงานตามปกติเมื่อออกแบบอินเตอร์เฟสของฟังก์ชันที่ไม่ใช่เทมเพลต:
ผ่านค่าหากฟังก์ชันไม่ต้องการแก้ไขพารามิเตอร์และค่าถูกคัดลอก (int, double, float, char, bool, etc ... สังเกตว่า std :: string, std :: vector, และส่วนที่เหลือ ของคอนเทนเนอร์ในไลบรารีมาตรฐานไม่ใช่)
ส่งผ่านตัวชี้ const หากค่ามีราคาแพงในการคัดลอกและฟังก์ชั่นไม่ต้องการแก้ไขค่าที่ชี้ไปและ NULL เป็นค่าที่ฟังก์ชันจัดการ
ผ่านตัวชี้ที่ไม่ใช่ const ถ้าค่ามีราคาแพงในการคัดลอกและฟังก์ชันต้องการแก้ไขค่าที่ชี้ไปและ NULL เป็นค่าที่ฟังก์ชันจัดการ
ผ่านการอ้างอิง const เมื่อค่ามีราคาแพงในการคัดลอกและฟังก์ชั่นไม่ต้องการแก้ไขค่าที่อ้างถึงและ NULL จะไม่เป็นค่าที่ถูกต้องหากใช้ตัวชี้แทน
ผ่านการอ้างอิงที่ไม่ใช่ const เมื่อค่ามีราคาแพงในการคัดลอกและฟังก์ชันต้องการแก้ไขค่าที่อ้างถึงและ NULL จะไม่ใช่ค่าที่ถูกต้องหากใช้ตัวชี้แทน
std::optional
ไปที่รูปภาพและคุณไม่ต้องการตัวชี้อีกต่อไป
ดูเหมือนคุณจะได้คำตอบ การส่งผ่านค่ามีราคาแพง แต่ให้สำเนาเพื่อทำงานหากคุณต้องการ
เป็นกฎที่ผ่านไปโดยอ้างอิง const จะดีกว่า แต่ถ้าคุณต้องการแก้ไขอาร์กิวเมนต์ของฟังก์ชันในเครื่องคุณควรใช้การผ่านค่า สำหรับประเภทพื้นฐานบางประเภทประสิทธิภาพโดยทั่วไปจะเหมือนกันทั้งสำหรับการส่งผ่านค่าและอ้างอิง การอ้างอิงตัวชี้ภายในโดยพอยน์เตอร์จริงๆแล้วนั่นคือสาเหตุที่คุณสามารถคาดหวังได้ว่าอินสแตนซ์สำหรับพอยน์เตอร์การส่งผ่านทั้งสองจะเหมือนกันในแง่ของประสิทธิภาพการทำงานหรือแม้แต่การส่งผ่านค่า
ในฐานะที่เป็นกฎของหัวแม่มือค่าสำหรับประเภทที่ไม่ใช่ชั้นเรียนและการอ้างอิง const สำหรับชั้นเรียน ถ้าชั้นเรียนมีขนาดเล็กจริง ๆ น่าจะดีกว่าที่จะผ่านค่า แต่ความแตกต่างนั้นน้อยที่สุด สิ่งที่คุณต้องการหลีกเลี่ยงจริง ๆ คือผ่านคลาสมโหฬารโดยมีค่าและมีการทำซ้ำทั้งหมด - นี้จะสร้างความแตกต่างใหญ่ถ้าคุณผ่านพูด std :: vector กับองค์ประกอบบางอย่างในนั้น
std::vector
จัดสรรสิ่งของในกองและวัตถุเวกเตอร์นั้นไม่เคยเติบโต โอ้เดี๋ยวก่อน หากการดำเนินการทำให้สำเนาของเวกเตอร์ที่จะทำ แต่ในความเป็นจริงมันจะไปและทำซ้ำองค์ประกอบทั้งหมด มันจะไม่ดี
sizeof(std::vector<int>)
เป็นค่าคงที่ แต่การส่งผ่านค่าจะยังคงคัดลอกเนื้อหาในกรณีที่ไม่มีคอมไพเลอร์ใด ๆ
ผ่านค่าขนาดเล็ก
ผ่านการอ้างอิง const สำหรับประเภทใหญ่ (คำจำกัดความของขนาดใหญ่อาจแตกต่างกันระหว่างเครื่อง) BUT ใน C ++ 11 ส่งผ่านค่าถ้าคุณจะใช้ข้อมูลเนื่องจากคุณสามารถใช้ประโยชน์จากการย้ายความหมาย ตัวอย่างเช่น:
class Person {
public:
Person(std::string name) : name_(std::move(name)) {}
private:
std::string name_;
};
ตอนนี้รหัสโทรจะทำ:
Person p(std::string("Albert"));
และมีเพียงวัตถุเดียวเท่านั้นที่จะถูกสร้างและย้ายไปยังสมาชิกname_
ในคลาสPerson
โดยตรง หากคุณผ่านการอ้างอิง const name_
สำเนาจะต้องมีการสร้างขึ้นมาเพื่อใส่ลงใน
ความแตกต่างง่าย ๆ : - ในฟังก์ชั่นเรามีพารามิเตอร์อินพุตและเอาต์พุตดังนั้นหากพารามิเตอร์อินพุตและเอาต์พุตของคุณเหมือนกันให้ใช้การอ้างอิงโดยการอ้างอิงอื่นหากพารามิเตอร์อินพุตและเอาต์พุตแตกต่างกัน
ตัวอย่าง void amount(int account , int deposit , int total )
พารามิเตอร์อินพุต: บัญชี, พารามิเตอร์การส่งออกเงินฝาก: ทั้งหมด
อินพุตและเอ้าท์คือการใช้งานที่แตกต่างกันโดย vaule
void amount(int total , int deposit )
ยอดรวมเงินฝากทั้งหมด