ฉันควรผ่านฟังก์ชั่น std :: โดยการอ้างอิง const หรือไม่


141

สมมติว่าฉันมีฟังก์ชั่นที่ใช้std::function:

void callFunction(std::function<void()> x)
{
    x();
}

ฉันควรผ่านxการอ้างอิง const แทนไหม:

void callFunction(const std::function<void()>& x)
{
    x();
}

คำตอบของคำถามนี้เปลี่ยนไปตามฟังก์ชั่นนี้หรือไม่? ตัวอย่างเช่นถ้ามันเป็นฟังก์ชั่นสมาชิกระดับหรือคอนสตรัคที่เก็บหรือเริ่มต้นstd::functionเป็นตัวแปรสมาชิก


1
อาจจะไม่. ฉันไม่รู้แน่นอน แต่ฉันคาดว่าsizeof(std::function)จะไม่เกิน2 * sizeof(size_t)ซึ่งเป็นขนาดที่เล็กที่สุดที่คุณเคยพิจารณาสำหรับการอ้างอิง const
Mats Petersson

12
@ Mats: ฉันไม่คิดว่าขนาดของเครื่องstd::functionห่อหุ้มมีความสำคัญเท่ากับความซับซ้อนของการคัดลอก หากเกี่ยวข้องกับสำเนาลึกอาจมีราคาแพงกว่าคำsizeofแนะนำ
Ben Voigt

คุณควรmoveใช้ฟังก์ชันนี้หรือไม่?
Yakk - Adam Nevraumont

operator()()คือconstเพื่อการอ้างอิง const ควรจะทำงาน แต่ฉันไม่เคยใช้ std :: function
Neel Basu

@Yakk ฉันเพิ่งผ่านแลมบ์ดาไปยังฟังก์ชั่นโดยตรง
Sven Adbring

คำตอบ:


79

หากคุณต้องการประสิทธิภาพให้ส่งค่าถ้าคุณเก็บไว้

สมมติว่าคุณมีฟังก์ชันที่เรียกว่า "เรียกใช้สิ่งนี้ในเธรด UI"

std::future<void> run_in_ui_thread( std::function<void()> )

ซึ่งรันโค้ดบางอย่างในเธรด "ui" จากนั้นส่งสัญญาณfutureเมื่อเสร็จแล้ว (มีประโยชน์ในกรอบ UI ที่เธรด UI คือตำแหน่งที่คุณควรยุ่งกับองค์ประกอบ UI)

เรามีสองลายเซ็นต์ที่เรากำลังพิจารณา:

std::future<void> run_in_ui_thread( std::function<void()> ) // (A)
std::future<void> run_in_ui_thread( std::function<void()> const& ) // (B)

ตอนนี้เราน่าจะใช้สิ่งเหล่านี้ดังนี้:

run_in_ui_thread( [=]{
  // code goes here
} ).wait();

ซึ่งจะสร้างการปิดนิรนาม (แลมบ์ดา) สร้างstd::functionออกมาส่งผ่านไปยังrun_in_ui_threadฟังก์ชั่นจากนั้นรอให้มันเสร็จสิ้นการทำงานในเธรดหลัก

ในกรณีที่ (A) ที่ถูกสร้างโดยตรงจากแลมบ์ดาของเราซึ่งถูกนำมาใช้แล้วภายในstd::function run_in_ui_threadแลมบ์ดานั้นmoveเข้าสู่std::functionดังนั้นสถานะเคลื่อนย้ายใด ๆ จะถูกดำเนินการอย่างมีประสิทธิภาพ

ในกรณีที่สองชั่วคราวstd::functionถูกสร้างขึ้นแลมบ์ดาเป็นmoveวันที่เป็นมันแล้วว่าชั่วคราวจะถูกใช้โดยการอ้างอิงภายในstd::functionrun_in_ui_thread

จนถึงตอนนี้ดีมาก - พวกเขาสองคนทำงานได้เหมือนกัน ยกเว้นrun_in_ui_threadจะทำสำเนาของอาร์กิวเมนต์ฟังก์ชั่นเพื่อส่งไปยังเธรด UI เพื่อดำเนินการ! (มันจะกลับมาก่อนที่มันจะเสร็จสิ้นดังนั้นจึงไม่สามารถใช้การอ้างอิงกับมันได้) สำหรับกรณี (A) เราก็ลงในระยะยาวการจัดเก็บข้อมูล ในกรณี (B), เราถูกบังคับให้คัดลอกmovestd::functionstd::function

ร้านค้านั้นทำให้การส่งผ่านโดยมีมูลค่าเหมาะสมที่สุด หากมีความเป็นไปได้ใด ๆ ที่คุณกำลังเก็บสำเนาของstd::functionผ่านค่า มิฉะนั้นทั้งสองวิธีนั้นจะเทียบเท่ากันโดยประมาณ: ข้อเสียเพียงข้อเดียวคือถ้าคุณใช้วิธีการที่ใหญ่std::functionและมีวิธีย่อยหนึ่งหลังจากใช้วิธีอื่น แบริ่งนั้นจะเป็นที่มีประสิทธิภาพเป็นmoveconst&

ขณะนี้มีบางอย่างที่แตกต่างอื่น ๆ std::functionระหว่างคนทั้งสองที่ส่วนใหญ่จะเตะในถ้าเรามีรัฐถาวรภายใน

สมมติว่าstd::functionเก็บวัตถุบางอย่างด้วยoperator() constแต่ก็มีmutableสมาชิกข้อมูลบางส่วนที่มันปรับเปลี่ยน (วิธีหยาบคาย!)

ในstd::function<> const&กรณีmutableที่สมาชิกข้อมูลที่แก้ไขจะเผยแพร่จากการเรียกใช้ฟังก์ชัน ในstd::function<>กรณีที่พวกเขาจะไม่

นี่เป็นกรณีมุมที่ค่อนข้างแปลก

คุณต้องการที่จะปฏิบัติstd::functionเหมือนคนอื่น ๆ ที่อาจเป็นประเภทที่มีน้ำหนักมากและเคลื่อนย้ายได้อย่างถูก การย้ายมีราคาถูกการคัดลอกอาจมีราคาแพง


ข้อได้เปรียบทางความหมายของ "pass by value หากคุณเก็บไว้" ตามที่คุณพูดคือว่าโดยสัญญาฟังก์ชันไม่สามารถเก็บที่อยู่ของอาร์กิวเมนต์ที่ส่งผ่านได้ แต่มันเป็นความจริงหรือไม่ที่ "ยกเว้นว่าการเคลื่อนไหวจะมีประสิทธิภาพเท่ากับกลุ่ม &"? ฉันมักจะเห็นค่าใช้จ่ายของการดำเนินการคัดลอกบวกกับค่าใช้จ่ายของการดำเนินการย้าย เมื่อผ่านไปconst&ฉันเห็นเฉพาะค่าใช้จ่ายในการทำสำเนา
ceztko

2
@ceztko ในทั้งสองกรณี (A) และ (B) ชั่วคราวstd::functionจะถูกสร้างขึ้นจากแลมบ์ดา ใน (A) ชั่วคราวจะ elided run_in_ui_threadโต้เถียงไป ใน (ข) run_in_ui_threadการอ้างอิงไปยังกล่าวชั่วคราวจะถูกส่งไป ตราบใดที่คุณstd::functionยังถูกสร้างขึ้นจาก lambdas เหมือนขมับประโยคนั้นถือ ย่อหน้าก่อนหน้าเกี่ยวข้องกับกรณีที่std::functionยังคงมีอยู่ หากเราไม่ได้จัดเก็บเพียงแค่สร้างจากแลมบ์ดาfunction const&และfunctionมีค่าใช้จ่ายที่แน่นอน
Yakk - Adam Nevraumont

อ่าฉันเข้าใจแล้ว! run_in_ui_thread()นี้แน่นอนขึ้นอยู่กับสิ่งที่เกิดขึ้นนอก มีเพียงลายเซ็นที่จะพูดว่า "ผ่านการอ้างอิง แต่ฉันจะไม่เก็บที่อยู่" หรือไม่?
ceztko

@ceztko ไม่ไม่มี
Yakk - Adam Nevraumont

1
@ Yakk-AdamNevraumont หากจะสมบูรณ์มากขึ้นเพื่อให้ครอบคลุมตัวเลือกอื่นที่จะผ่านการอ้างอิง rvalue:std::future<void> run_in_ui_thread( std::function<void()>&& )
Pavel P

33

หากคุณกังวลเกี่ยวกับประสิทธิภาพและคุณไม่ได้กำหนดฟังก์ชั่นสมาชิกเสมือนแสดงว่าคุณไม่ควรใช้std::functionงานเลย

การทำให้ functor พิมพ์พารามิเตอร์เทมเพลตช่วยเพิ่มประสิทธิภาพได้มากกว่าstd::functionรวมถึงการทำอินไลน์ตรรกะ functor ผลกระทบของการเพิ่มประสิทธิภาพเหล่านี้น่าจะมากเกินดุลกังวลคัดลอก std::functionVS-ร้ายเกี่ยวกับวิธีการที่จะผ่าน

ได้เร็วขึ้น:

template<typename Functor>
void callFunction(Functor&& x)
{
    x();
}

1
ฉันไม่ได้กังวลเลยเกี่ยวกับการแสดงจริงๆ ฉันแค่คิดว่าใช้การอ้างอิง const ที่ควรใช้เป็นเรื่องธรรมดา (สตริงและเวกเตอร์มาถึงใจ)
Sven Adbring

13
@Ben: ฉันคิดว่าวิธีที่เป็นมิตรกับฮิปปี้ที่ทันสมัยที่สุดในการใช้งานนี้คือการใช้std::forward<Functor>(x)();เพื่อรักษาหมวดหมู่ค่าของ functor เนื่องจากเป็นข้อมูลอ้างอิง "สากล" แม้ว่าจะไม่สร้างความแตกต่างใน 99% ของคดี แต่อย่างใด
GManNickG

1
@Ben Voigt ดังนั้นสำหรับกรณีของคุณฉันจะเรียกฟังก์ชั่นด้วยการย้ายหรือไม่? callFunction(std::move(myFunctor));
arias_JC

2
@arias_JC: หากพารามิเตอร์เป็นแลมบ์ดามันเป็นค่า rvalue อยู่แล้ว หากคุณมีค่า lvalue คุณสามารถใช้std::moveหากคุณไม่ต้องการใช้วิธีอื่นหรือส่งผ่านโดยตรงหากคุณไม่ต้องการย้ายออกจากวัตถุที่มีอยู่ กฎการยุบตัวอ้างอิงทำให้มั่นใจได้ว่าcallFunction<T&>()มีพารามิเตอร์ของชนิดไม่T& T&&
Ben Voigt

1
@BoltzmannBrain: ฉันเลือกที่จะไม่ทำการเปลี่ยนแปลงเพราะมันใช้ได้กับกรณีที่ง่ายที่สุดเท่านั้นเมื่อฟังก์ชั่นถูกเรียกเพียงครั้งเดียว คำตอบของฉันคือคำถามที่ว่า "ฉันจะส่งผ่านฟังก์ชันวัตถุได้อย่างไร" และไม่ จำกัด เพียงฟังก์ชั่นที่ไม่ทำอะไรเลยนอกจากเรียกใช้ functor เพียงครั้งเดียวโดยไม่มีเงื่อนไข
Ben Voigt

25

ตามปกติใน C ++ 11 การส่งผ่านค่า / การอ้างอิง / การอ้างอิง const ขึ้นอยู่กับสิ่งที่คุณทำกับอาร์กิวเมนต์ของคุณ std::functionไม่แตกต่างกัน

การผ่านค่าอนุญาตให้คุณย้ายอาร์กิวเมนต์ไปยังตัวแปร (โดยทั่วไปคือตัวแปรสมาชิกของคลาส):

struct Foo {
    Foo(Object o) : m_o(std::move(o)) {}

    Object m_o;
};

เมื่อคุณรู้ว่าฟังก์ชั่นของคุณจะย้ายอาร์กิวเมนต์มันเป็นทางออกที่ดีที่สุดวิธีนี้ผู้ใช้ของคุณสามารถควบคุมวิธีที่พวกเขาเรียกฟังก์ชั่นของคุณ:

Foo f1{Object()};               // move the temporary, followed by a move in the constructor
Foo f2{some_object};            // copy the object, followed by a move in the constructor
Foo f3{std::move(some_object)}; // move the object, followed by a move in the constructor

ฉันเชื่อว่าคุณรู้ความหมายของการอ้างอิง const (ไม่ใช่) ดังนั้นฉันจะไม่เชื่อประเด็น หากคุณต้องการให้ฉันเพิ่มคำอธิบายเพิ่มเติมเกี่ยวกับสิ่งนี้เพียงแค่ถามและฉันจะอัปเดต

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