ฉันควรใช้ std :: function หรือ function pointer ใน C ++ หรือไม่


142

เมื่อใช้ฟังก์ชั่นการโทรกลับใน C ++ ฉันควรจะใช้ตัวชี้ฟังก์ชั่น C-style:

void (*callbackFunc)(int);

หรือฉันควรใช้ประโยชน์จากฟังก์ชั่น std ::

std::function< void(int) > callbackFunc;

9
หากทราบว่าฟังก์ชันการโทรกลับในเวลารวบรวมให้พิจารณาเทมเพลตแทน
Baum mit Augen

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

คำตอบ:


171

ในระยะสั้นใช้std::functionจนกว่าคุณจะมีเหตุผลที่จะไม่

ตัวชี้ฟังก์ชั่นมีข้อเสียของการไม่สามารถจับภาพบริบทบางอย่าง คุณจะไม่สามารถส่งผ่านฟังก์ชั่นแลมบ์ดาเป็นโทรกลับซึ่งจับตัวแปรบริบทบางอย่าง (แต่จะใช้งานได้หากไม่จับภาพใด ๆ ) การเรียกตัวแปรสมาชิกของวัตถุ (เช่นไม่คงที่) จึงเป็นไปไม่ได้เช่นกันเนื่องจากวัตถุ ( this-pointer) ต้องถูกจับ (1)

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

ลองนึกถึงตัวเลือกที่สาม: หากคุณกำลังจะใช้ฟังก์ชั่นขนาดเล็กซึ่งจะรายงานบางอย่างผ่านฟังก์ชั่นการโทรกลับที่ให้มาให้พิจารณาพารามิเตอร์เทมเพลตซึ่งอาจเป็นวัตถุที่เรียกได้เช่นตัวชี้ฟังก์ชัน functor แลมบ์ดา a std::function, ... ข้อเสียเปรียบที่นี่คือฟังก์ชั่น (ด้านนอก) ของคุณจะกลายเป็นแม่แบบ ในทางกลับกันคุณจะได้รับประโยชน์จากการโทรกลับไปยัง inline เนื่องจากโค้ดไคลเอนต์ของฟังก์ชัน (outer) ของคุณ "เห็น" การเรียกไปที่ callback จะเป็นข้อมูลประเภทที่แน่นอน

ตัวอย่างสำหรับรุ่นที่มีพารามิเตอร์เทมเพลต (เขียน&แทน&&pre-C ++ 11):

template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
    ...
    callback(...);
    ...
}

ดังที่คุณเห็นในตารางต่อไปนี้ทุกคนมีข้อดีและข้อเสีย:

+-------------------+--------------+---------------+----------------+
|                   | function ptr | std::function | template param |
+===================+==============+===============+================+
| can capture       |    no(1)     |      yes      |       yes      |
| context variables |              |               |                |
+-------------------+--------------+---------------+----------------+
| no call overhead  |     yes      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be inlined    |      no      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be stored     |     yes      |      yes      |      no(2)     |
| in class member   |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be implemented|     yes      |      yes      |       no       |
| outside of header |              |               |                |
+-------------------+--------------+---------------+----------------+
| supported without |     yes      |     no(3)     |       yes      |
| C++11 standard    |              |               |                |
+-------------------+--------------+---------------+----------------+
| nicely readable   |      no      |      yes      |      (yes)     |
| (my opinion)      | (ugly type)  |               |                |
+-------------------+--------------+---------------+----------------+

(1) การแก้ไขปัญหามีอยู่เพื่อเอาชนะข้อ จำกัด นี้ตัวอย่างเช่นการส่งข้อมูลเพิ่มเติมเป็นพารามิเตอร์เพิ่มเติมไปยังฟังก์ชัน (ภายนอก) ของคุณ: myFunction(..., callback, data)จะเรียกcallback(data)ใช้ นั่นคือ C-style "callback with arguments" ซึ่งเป็นไปได้ใน C ++ (และตามวิธีที่ใช้อย่างหนักใน WIN32 API) แต่ควรหลีกเลี่ยงเพราะเรามีตัวเลือกที่ดีกว่าใน C ++

(2) เว้นแต่ว่าเรากำลังพูดถึงเทมเพลตของคลาสนั่นคือคลาสที่คุณเก็บฟังก์ชันนั้นเป็นเทมเพลต แต่นั่นหมายความว่าในฝั่งไคลเอ็นต์ประเภทของฟังก์ชั่นจะตัดสินใจชนิดของวัตถุที่เก็บการติดต่อกลับซึ่งแทบจะไม่เคยมีตัวเลือกสำหรับกรณีการใช้งานจริง

(3) สำหรับ pre-C ++ 11 ให้ใช้ boost::function


9
ตัวชี้ฟังก์ชั่นมีค่าใช้จ่ายการโทรเมื่อเทียบกับพารามิเตอร์แม่แบบ พารามิเตอร์เทมเพลตทำให้การทำอินไลน์เป็นเรื่องง่ายแม้ว่าคุณจะผ่านระดับสิบไปแล้วเพราะรหัสที่ถูกดำเนินการถูกอธิบายโดยประเภทของพารามิเตอร์ไม่ใช่ค่า และฟังก์ชั่นแม่แบบฟังก์ชั่นวัตถุที่ถูกเก็บไว้ในประเภทคืนเท็มเพลตเป็นรูปแบบทั่วไปและมีประโยชน์ (ด้วยตัวสร้างสำเนาที่ดีคุณสามารถสร้างฟังก์ชั่นแม่แบบที่มีประสิทธิภาพที่สามารถแปลงเป็นstd::functionชนิดลบได้หากคุณต้องการ เรียกว่าบริบท)
Yakk - Adam Nevraumont

1
@tohecz ฉันพูดถึงว่าต้องใช้ C ++ 11 หรือไม่
leemes

1
@Yakk โอ้แน่นอนลืมไปแล้ว! เพิ่มมันขอบคุณ
leemes

1
@MooingDuck แน่นอนมันขึ้นอยู่กับการใช้งาน แต่ถ้าฉันจำได้อย่างถูกต้องเนื่องจากการลบประเภททำงานอย่างไร แต่ตอนนี้ที่ฉันคิดเกี่ยวกับมันอีกครั้งผมคิดว่ากรณีนี้ไม่ได้ถ้าคุณคำแนะนำการทำงานกำหนดหรือจับน้อย lambdas กับมัน ... (ตามการเพิ่มประสิทธิภาพทั่วไป)
leemes

1
@leemes: ถูกต้องสำหรับพอยน์เตอร์ของฟังก์ชั่นหรือ lambdas ที่ไม่มีตัวตนมันควรจะมีค่าใช้จ่ายเช่นเดียวกับ c-func-ptr ซึ่งยังคงเป็นแผงลอยไปป์ไลน์ + ไม่ inlined เล็กน้อย
Mooing Duck

25

void (*callbackFunc)(int); อาจเป็นฟังก์ชั่นโทรกลับ C สไตล์ แต่มันเป็นหนึ่งในการออกแบบที่ไม่ดีอย่างน่ากลัว

การเรียกกลับสไตล์ C ที่ออกแบบมาอย่างดีดูเหมือนว่าvoid (*callbackFunc)(void*, int);จะมีการvoid*อนุญาตให้รหัสที่โทรกลับเพื่อรักษาสถานะเกินฟังก์ชั่น การไม่ทำเช่นนี้เป็นการบังคับให้ผู้เรียกเก็บสถานะทั่วโลกซึ่งไม่สุภาพ

std::function< int(int) >จบลงด้วยการเป็นราคาแพงกว่าการint(*)(void*, int)ขอร้องในการใช้งานส่วนใหญ่ อย่างไรก็ตามมันเป็นเรื่องยากสำหรับคอมไพเลอร์บางส่วนที่จะอยู่ในบรรทัด มีstd::functionการนำไปใช้งานแบบโคลนนิ่งที่คู่แข่งเรียกใช้ฟังก์ชันการชี้เหนือหัว (ดูที่ 'ตัวแทนที่เร็วที่สุดที่เป็นไปได้' เป็นต้น) ที่อาจนำไปใช้ในห้องสมุด

ตอนนี้ไคลเอนต์ของระบบติดต่อกลับมักจะต้องตั้งค่าทรัพยากรและกำจัดพวกเขาเมื่อมีการสร้างและลบโทรกลับและเพื่อให้ตระหนักถึงอายุการใช้งานของการโทรกลับ void(*callback)(void*, int)ไม่ได้ให้สิ่งนี้

บางครั้งสิ่งนี้สามารถใช้ได้ผ่านโครงสร้างรหัส (การโทรกลับมี จำกัด อายุการใช้งาน) หรือผ่านกลไกอื่น ๆ (ยกเลิกการลงทะเบียนการโทรกลับและสิ่งที่คล้ายกัน)

std::function จัดเตรียมวิธีสำหรับการจัดการอายุการใช้งานที่ จำกัด (สำเนาล่าสุดของวัตถุจะหายไปเมื่อถูกลืม)

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

ฉันส่วนใหญ่จะใช้ตัวชี้ฟังก์ชั่นสำหรับ API ดั้งเดิมเท่านั้นหรือสำหรับการสร้างอินเตอร์เฟส C สำหรับการสื่อสารระหว่างคอมไพเลอร์ที่สร้างรหัส ฉันยังใช้พวกเขาเป็นรายละเอียดการใช้งานภายในเมื่อฉันใช้ตารางกระโดดประเภทลบ ฯลฯ : เมื่อฉันทั้งการผลิตและการบริโภคและฉันไม่เปิดเผยภายนอกสำหรับรหัสลูกค้าที่จะใช้และตัวชี้ฟังก์ชั่นทำทุกอย่างที่ฉันต้องการ .

โปรดทราบว่าคุณสามารถเขียนชุดที่เปลี่ยนstd::function<int(int)>เป็นint(void*,int)รูปแบบการโทรกลับโดยสมมติว่ามีโครงสร้างพื้นฐานการจัดการอายุการใช้งานการโทรกลับที่เหมาะสม ดังนั้นในการทดสอบควันสำหรับระบบการจัดการการโทรกลับแบบ C สไตล์ใด ๆ ฉันจะตรวจสอบให้แน่ใจว่าการห่อนั้นใช้std::functionงานได้ดีพอสมควร


1
สิ่งนี้void*มาจากไหน ทำไมคุณต้องการที่จะรักษาสถานะนอกเหนือจากฟังก์ชั่น? ฟังก์ชั่นควรมีรหัสทั้งหมดที่ต้องการฟังก์ชั่นทั้งหมดคุณเพียงแค่ส่งอาร์กิวเมนต์ที่ต้องการและแก้ไขและส่งคืนบางสิ่ง หากคุณต้องการสถานะภายนอกบางอย่างแล้วทำไม functionPtr หรือ callback จึงพกกระเป๋าใบนั้น ฉันคิดว่าการติดต่อกลับนั้นซับซ้อนโดยไม่จำเป็น
Nikos

@ nik-lz ฉันไม่แน่ใจว่าฉันจะสอนคุณเกี่ยวกับการใช้และประวัติการโทรกลับใน C ในความคิดเห็นอย่างไร หรือปรัชญาของขั้นตอนต่าง ๆ เมื่อเทียบกับการเขียนโปรแกรมฟังก์ชั่น ดังนั้นคุณจะไม่ได้เติมเต็ม
Yakk - Adam Nevraumont

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

ฟังก์ชันสมาชิก @ Nik-Lz ไม่ใช่ฟังก์ชั่น ฟังก์ชั่นไม่มีสถานะ (รันไทม์) การเรียกกลับใช้void*เพื่ออนุญาตให้ส่งผ่านสถานะรันไทม์ ตัวชี้ฟังก์ชั่นที่มีvoid*และvoid*อาร์กิวเมนต์สามารถเลียนแบบการเรียกใช้ฟังก์ชันสมาชิกไปยังวัตถุ ขออภัยฉันไม่รู้แหล่งข้อมูลที่เดินผ่าน "การออกแบบกลไกการติดต่อกลับ C 101"
Yakk - Adam Nevraumont

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

17

ใช้std::functionสำหรับเก็บวัตถุที่เรียกได้โดยไม่ตั้งใจ อนุญาตให้ผู้ใช้จัดเตรียมบริบทใด ๆ ก็ตามที่จำเป็นสำหรับการเรียกกลับ ตัวชี้ฟังก์ชั่นธรรมดาไม่ได้

หากคุณจำเป็นต้องใช้พอยน์เตอร์ฟังก์ชั่นธรรมดาด้วยเหตุผลบางอย่าง (อาจเป็นเพราะคุณต้องการ API ที่เข้ากันได้กับ C) ดังนั้นคุณควรเพิ่มvoid * user_contextอาร์กิวเมนต์เพื่อให้เป็นไปได้อย่างน้อยที่สุด (แม้ว่าจะไม่สะดวก) ฟังก์ชัน


p ประเภทนี่คืออะไร? มันจะเป็น std :: function type? เป็นโมฆะ f () {}; อัตโนมัติ p = f; P ();
sree

14

เหตุผลเดียวที่ควรหลีกเลี่ยงstd::functionคือการสนับสนุนของคอมไพเลอร์ดั้งเดิมที่ขาดการสนับสนุนสำหรับเทมเพลตนี้ซึ่งได้รับการแนะนำใน C ++ 11

หากการสนับสนุนภาษา pre-C ++ 11 ไม่ใช่ข้อกำหนดการใช้std::functionจะช่วยให้ผู้โทรของคุณมีทางเลือกมากขึ้นในการใช้การติดต่อกลับทำให้เป็นตัวเลือกที่ดีกว่าเมื่อเทียบกับพอยน์เตอร์ฟังก์ชัน "ธรรมดา" มันเสนอทางเลือกให้แก่ผู้ใช้ API ของคุณมากขึ้นในขณะที่แยกแยะข้อมูลเฉพาะของการใช้งานสำหรับโค้ดของคุณที่ดำเนินการติดต่อกลับ


1

std::function อาจนำรหัส VMT มาใช้ในบางกรณีซึ่งมีผลกระทบต่อประสิทธิภาพการทำงาน


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