Pass-by-value เป็นค่าเริ่มต้นที่สมเหตุสมผลใน C ++ 11 หรือไม่


142

ใน C ++ แบบดั้งเดิมการส่งผ่านค่าไปยังฟังก์ชั่นและวิธีการจะช้าสำหรับวัตถุขนาดใหญ่และโดยทั่วไปจะขมวดคิ้ว แต่โปรแกรมเมอร์ C ++ มักจะส่งต่อการอ้างอิงไปรอบ ๆ ซึ่งเร็วกว่า แต่แนะนำคำถามที่ซับซ้อนทุกประเภทเกี่ยวกับการเป็นเจ้าของและโดยเฉพาะการจัดการหน่วยความจำ (ในกรณีที่วัตถุจัดสรรเป็นฮีป)

ตอนนี้ใน C ++ 11 เรามี Rvalue reference และ move constructors ซึ่งหมายความว่ามันเป็นไปได้ที่จะใช้ object ขนาดใหญ่ (เช่นstd::vector) ที่ราคาถูกเพื่อส่งผ่านค่าเข้าและออกจากฟังก์ชัน

ดังนั้นนี่หมายความว่าค่าเริ่มต้นควรจะส่งผ่านตามค่าสำหรับอินสแตนซ์ของประเภทเช่นstd::vectorและstd::string? แล้ววัตถุที่กำหนดเองล่ะ? แนวปฏิบัติที่ดีที่สุดแบบใหม่คืออะไร


22
pass by reference ... which introduces all sorts of complicated questions around ownership and especially around memory management (in the event that the object is heap-allocated). ฉันไม่เข้าใจว่ามันซับซ้อนหรือมีปัญหาสำหรับการเป็นเจ้าของ? ฉันอาจจะพลาดอะไรไปหรือเปล่า
iammilind

1
@iammilind: ตัวอย่างจากประสบการณ์ส่วนตัว หนึ่งเธรดมีวัตถุสตริง มันถูกส่งผ่านไปยังฟังก์ชันที่วางไข่เธรดอื่น แต่ไม่ทราบถึงผู้เรียกฟังก์ชั่นใช้สตริงเป็นconst std::string&และไม่ใช่การคัดลอก จากนั้นเธรดแรกจะออก ...
Zan Lynx

12
@ZanLynx: ดูเหมือนฟังก์ชั่นที่ชัดเจนว่าไม่เคยถูกออกแบบมาให้เรียกว่าเป็นฟังก์ชั่นเธรด
Nicol Bolas

5
เห็นด้วยกับ iammilind ฉันไม่เห็นปัญหาใด ๆ ผ่านการอ้างอิง const ควรเป็นค่าเริ่มต้นของคุณสำหรับวัตถุ "ใหญ่" และโดยค่าสำหรับวัตถุขนาดเล็ก ฉันจะใส่ขีด จำกัด ระหว่างขนาดใหญ่และขนาดเล็กที่ประมาณ 16 ไบต์ (หรือ 4 พอยน์เตอร์ในระบบ 32 บิต)
JN

3
Herb Sutter กลับไปสู่พื้นฐาน! Essentials of Modern C ++ นำเสนอที่ CppCon ทำให้มีรายละเอียดค่อนข้างมาก วิดีโอที่นี่
Chris Drew

คำตอบ:


138

มันเป็นค่าเริ่มต้นที่สมเหตุสมผลหากคุณต้องการทำสำเนาภายในร่างกาย นี่คือสิ่งที่ Dave Abrahams ให้การสนับสนุน :

คำแนะนำ: อย่าคัดลอกอาร์กิวเมนต์ของฟังก์ชัน ให้ส่งพวกเขาด้วยค่าและให้คอมไพเลอร์ทำการคัดลอกแทน

ในรหัสนี่หมายความว่าอย่าทำสิ่งนี้:

void foo(T const& t)
{
    auto copy = t;
    // ...
}

แต่ทำสิ่งนี้:

void foo(T t)
{
    // ...
}

ซึ่งมีข้อดีที่ผู้โทรสามารถใช้ได้foo:

T lval;
foo(lval); // copy from lvalue
foo(T {}); // (potential) move from prvalue
foo(std::move(lval)); // (potential) move from xvalue

และทำงานเพียงเล็กน้อยเท่านั้น คุณจะต้องสอง overloads ที่จะทำเช่นเดียวกันกับการอ้างอิงและvoid foo(T const&);void foo(T&&);

โดยที่ในใจตอนนี้ฉันเขียน constructors ที่มีค่าของฉันเช่น:

class T {
    U u;
    V v;
public:
    T(U u, V v)
        : u(std::move(u))
        , v(std::move(v))
    {}
};

มิฉะนั้นการส่งต่อโดยอ้างอิงถึงconstยังคงสมเหตุสมผล


29
+1 โดยเฉพาะอย่างยิ่งสำหรับบิตสุดท้าย :) เราไม่ควรลืมว่า Move Constructors สามารถเรียกใช้ได้ก็ต่อเมื่อไม่คาดว่าวัตถุที่จะย้ายจากนั้นจะไม่ถูกเปลี่ยนหลังจากนั้น: SomeProperty p; for (auto x: vec) { x.foo(p); }ไม่พอดีตัวอย่างเช่น นอกจากนี้ตัวสร้างการเคลื่อนย้ายมีค่าใช้จ่าย (ยิ่งวัตถุมีขนาดใหญ่เท่าใดต้นทุนconst&ก็จะสูงขึ้น) ในขณะที่ไม่มีค่าใช้จ่าย
Matthieu M.

25
@MatthieuM แต่สิ่งสำคัญคือต้องทราบว่า "ยิ่งวัตถุใหญ่มากเท่าไหร่ค่าใช้จ่ายที่สูงขึ้น" ของการย้ายก็หมายถึง: "ใหญ่กว่า" จริง ๆ แล้วหมายถึง "ยิ่งสมาชิกตัวแปรมีมากขึ้น" ตัวอย่างเช่นการย้ายstd::vectorองค์ประกอบหนึ่งล้านรายการนั้นมีค่าใช้จ่ายเท่ากับการย้ายองค์ประกอบที่มีห้าองค์ประกอบเนื่องจากมีการย้ายเฉพาะตัวชี้ไปยังอาร์เรย์บนฮีปเท่านั้นไม่ใช่ทุกวัตถุในเวกเตอร์ ดังนั้นมันจึงไม่ใช่เรื่องใหญ่อะไรนัก
ลูคัส

+1 ฉันมีแนวโน้มที่จะใช้โครงสร้างแบบ pass-by-value-then-move ตั้งแต่ฉันเริ่มใช้ C ++ 11 นี้ทำให้ฉันรู้สึกไม่สบายใจบ้าง แต่เนื่องจากรหัสของฉันตอนนี้มีstd::moveทั่วทุกสถานที่ ..
Stijn

1
มีความเสี่ยงอย่างหนึ่งconst&ที่ทำให้ฉันสะดุดสองสามครั้ง void foo(const T&); int main() { S s; foo(s); }. สิ่งนี้สามารถรวบรวมได้แม้ว่าชนิดจะแตกต่างกันหากมีตัวสร้าง T ที่ใช้ S เป็นอาร์กิวเมนต์ สิ่งนี้อาจช้าเนื่องจากวัตถุ T ขนาดใหญ่อาจถูกสร้างขึ้น คุณอาจคิดว่าคุณผ่านการอ้างอิงโดยไม่คัดลอก แต่คุณอาจเป็น ดูคำตอบสำหรับคำถามที่ฉันถามเพิ่มเติมนี้ โดยทั่วไป&มักจะผูกเท่านั้นที่จะ lvalues rvalueแต่มีข้อยกเว้นสำหรับ มีทางเลือกอื่น
Aaron McDaid

1
@AaronMcDaid นี่เป็นข่าวเก่าในแง่ที่ว่าเป็นสิ่งที่คุณต้องรู้ก่อนหน้า C ++ 11 เสมอ และไม่มีอะไรเปลี่ยนแปลงไปมากนักเกี่ยวกับเรื่องนั้น
Luc Danton

71

ในเกือบทุกกรณีความหมายของคุณควรเป็น:

bar(foo f); // want to obtain a copy of f
bar(const foo& f); // want to read f
bar(foo& f); // want to modify f

ลายเซ็นอื่นทั้งหมดควรใช้เท่าที่จำเป็นและมีเหตุผลที่ดี คอมไพเลอร์จะทำงานเหล่านี้ในวิธีที่มีประสิทธิภาพมากที่สุด คุณสามารถเขียนโค้ดของคุณต่อไปได้!


2
แม้ว่าฉันจะชอบผ่านตัวชี้ถ้าฉันจะแก้ไขข้อโต้แย้ง ฉันเห็นด้วยกับคู่มือสไตล์ของ Google ว่าสิ่งนี้ทำให้เห็นได้ชัดเจนว่าการโต้แย้งจะถูกแก้ไขโดยไม่ต้องตรวจสอบลายเซ็นของฟังก์ชันอีกครั้ง ( google-styleguide.googlecode.com/svn/trunk/ … )
Max Lybbert

40
เหตุผลที่ฉันไม่ชอบพอยน์เตอร์ที่ผ่านคือเพิ่มสถานะความล้มเหลวที่เป็นไปได้ให้กับฟังก์ชั่นของฉัน ฉันพยายามเขียนฟังก์ชั่นทั้งหมดของฉันเพื่อให้ถูกต้องเพราะมันช่วยลดพื้นที่สำหรับข้อผิดพลาดในการซ่อนได้foo(bar& x) { x.a = 3; }เป็นห่ามากน่าเชื่อถือ (และอ่าน!) กว่าfoo(bar* x) {if (!x) throw std::invalid_argument("x"); x->a = 3;
Ayjay

22
@ Max Lybbert: ด้วยพารามิเตอร์ตัวชี้คุณไม่จำเป็นต้องตรวจสอบลายเซ็นของฟังก์ชั่น แต่คุณต้องตรวจสอบเอกสารเพื่อทราบว่าคุณได้รับอนุญาตให้ส่งผ่านพอยน์เตอร์พอยน์เตอร์หรือไม่ พารามิเตอร์ตัวชี้สื่อความหมายน้อยกว่าการอ้างอิงที่ไม่ใช่ const ฉันเห็นด้วยอย่างไรก็ตามมันเป็นการดีที่มีเงื่อนงำภาพที่ไซต์การโทรที่อาจมีการแก้ไขอาร์กิวเมนต์ (เช่นrefคำหลักใน C #)
Luc Touraille

ในเรื่องที่เกี่ยวกับการส่งผ่านคุณค่าและการใช้ความหมายของการย้ายผมรู้สึกว่าตัวเลือกทั้งสามนี้ทำได้ดีกว่าในการอธิบายการใช้พารามิเตอร์ นี่เป็นแนวทางที่ฉันมักจะปฏิบัติตามเช่นกัน
เทรเวอร์ Hickey

1
@AaronMcDaid is shared_ptr intended to never be null? Much as (I think) unique_ptr is?ข้อสมมติฐานทั้งสองไม่ถูกต้อง unique_ptrและshared_ptrสามารถถือโมฆะ / nullptrค่า หากคุณไม่ต้องการกังวลเกี่ยวกับค่า Null คุณควรใช้การอ้างอิงเนื่องจากไม่สามารถเป็นค่าว่างได้ คุณไม่ต้องพิมพ์->ซึ่งคุณคิดว่าน่ารำคาญ :)
Julian

10

ผ่านพารามิเตอร์ตามค่าถ้าภายในตัวฟังก์ชันคุณต้องการสำเนาของวัตถุหรือต้องการย้ายวัตถุเท่านั้น ผ่านconst&หากคุณต้องการเข้าถึงวัตถุที่ไม่กลายพันธุ์เท่านั้น

ตัวอย่างการคัดลอกวัตถุ:

void copy_antipattern(T const& t) { // (Don't do this.)
    auto copy = t;
    t.some_mutating_function();
}

void copy_pattern(T t) { // (Do this instead.)
    t.some_mutating_function();
}

ตัวอย่างการย้ายวัตถุ:

std::vector<T> v; 

void move_antipattern(T const& t) {
    v.push_back(t); 
}

void move_pattern(T t) {
    v.push_back(std::move(t)); 
}

ตัวอย่างการเข้าถึงที่ไม่กลายพันธุ์:

void read_pattern(T const& t) {
    t.some_const_function();
}

สำหรับเหตุผลดูบล็อกโพสต์เหล่านี้โดยเดฟอับราฮัมและXiang พัดลม


0

ลายเซ็นของฟังก์ชั่นควรสะท้อนให้เห็นถึงการใช้งานที่ตั้งใจไว้ การอ่านเป็นสิ่งสำคัญเช่นกันสำหรับเครื่องมือเพิ่มประสิทธิภาพ

นี่เป็นเงื่อนไขที่ดีที่สุดสำหรับเครื่องมือเพิ่มประสิทธิภาพในการสร้างรหัสที่เร็วที่สุด - ในทางทฤษฎีอย่างน้อยที่สุดและถ้าไม่ใช่ในความเป็นจริงแล้วในความเป็นจริงไม่กี่ปี

ข้อควรพิจารณาด้านประสิทธิภาพมักถูกประเมินค่ามากเกินไปในบริบทของการส่งพารามิเตอร์ การส่งต่อที่สมบูรณ์แบบเป็นตัวอย่าง ฟังก์ชั่นที่ชอบemplace_backนั้นสั้นมาก ๆ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.