Haruskah saya menggunakan std :: function atau function pointer di C ++?

148

Saat mengimplementasikan fungsi callback di C ++, apakah saya harus tetap menggunakan pointer fungsi C-style:

void (*callbackFunc)(int);

Atau haruskah saya menggunakan std :: function:

std::function< void(int) > callbackFunc;
Jan Swart
sumber
10
Jika fungsi callback diketahui pada waktu kompilasi, pertimbangkan template sebagai gantinya.
Baum mit Augen
4
Saat mengimplementasikan fungsi callback, Anda harus melakukan apa pun yang diminta pemanggil. Jika pertanyaan Anda benar-benar tentang merancang antarmuka panggilan balik, tidak ada informasi yang cukup di sini untuk menjawabnya. Apa yang Anda ingin penerima panggilan balik lakukan? Informasi apa yang perlu Anda berikan kepada penerima? Informasi apa yang harus diteruskan penerima kepada Anda sebagai hasil dari panggilan tersebut?
Pete Becker

Jawaban:

175

Singkatnya, gunakanstd::function kecuali Anda punya alasan untuk tidak melakukannya.

Pointer fungsi memiliki kelemahan karena tidak dapat menangkap beberapa konteks. Anda tidak akan dapat, misalnya, meneruskan fungsi lambda sebagai callback yang menangkap beberapa variabel konteks (tetapi akan berfungsi jika tidak menangkap apa pun). Memanggil variabel anggota suatu objek (yaitu non-statis) juga tidak dimungkinkan, karena objek ( this-pointer) perlu ditangkap. (1)

std::function(karena C ++ 11) pada dasarnya untuk menyimpan sebuah fungsi (menyebarkannya tidak mengharuskannya untuk disimpan). Karenanya, jika Anda ingin menyimpan callback misalnya dalam variabel anggota, itu mungkin pilihan terbaik Anda. Tetapi juga jika Anda tidak menyimpannya, ini adalah "pilihan pertama" yang baik meskipun memiliki kelemahan dalam memperkenalkan beberapa overhead (sangat kecil) saat dipanggil (jadi dalam situasi yang sangat kritis terhadap kinerja ini mungkin menjadi masalah tetapi di sebagian besar seharusnya tidak). Ini sangat "universal": jika Anda sangat peduli tentang kode yang konsisten dan dapat dibaca serta tidak ingin memikirkan setiap pilihan yang Anda buat (yaitu ingin membuatnya tetap sederhana), gunakan std::functionuntuk setiap fungsi yang Anda bagikan.

Pikirkan tentang opsi ketiga: Jika Anda akan mengimplementasikan fungsi kecil yang kemudian melaporkan sesuatu melalui fungsi panggilan balik yang disediakan, pertimbangkan parameter template , yang kemudian dapat berupa objek yang dapat dipanggil , yaitu penunjuk fungsi, functor, lambda, a std::function, ... Kekurangannya di sini adalah bahwa fungsi (luar) Anda menjadi template dan karenanya perlu diimplementasikan di header. Di sisi lain, Anda mendapatkan keuntungan bahwa panggilan ke callback bisa menjadi inline, karena kode klien dari fungsi (luar) Anda "melihat" panggilan ke callback akan menyediakan informasi jenis yang tepat.

Contoh untuk versi dengan parameter template (tulis &sebagai ganti &&untuk pra-C ++ 11):

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

Seperti yang Anda lihat pada tabel berikut, semuanya memiliki kelebihan dan kekurangan:

+-------------------+--------------+---------------+----------------+
|                   | 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) Ada solusi untuk mengatasi batasan ini, misalnya meneruskan data tambahan sebagai parameter lebih lanjut ke fungsi (luar) Anda: myFunction(..., callback, data)akan memanggil callback(data). Itu adalah "callback dengan argumen" gaya-C, yang dimungkinkan di C ++ (dan dengan cara yang banyak digunakan di WIN32 API) tetapi harus dihindari karena kita memiliki opsi yang lebih baik di C ++.

(2) Kecuali jika kita berbicara tentang templat kelas, yaitu kelas tempat Anda menyimpan fungsi adalah templat. Tetapi itu berarti bahwa di sisi klien, jenis fungsi memutuskan jenis objek yang menyimpan callback, yang hampir tidak pernah menjadi opsi untuk kasus penggunaan yang sebenarnya.

(3) Untuk pra-C ++ 11, gunakan boost::function

leemes
sumber
9
pointer fungsi memiliki overhead panggilan dibandingkan dengan parameter template. parameter template mempermudah penyebarisan, bahkan jika Anda diturunkan ke lebih dari satu tingkat, karena kode yang dijalankan dijelaskan oleh jenis parameter bukan nilainya. Dan objek fungsi templat yang disimpan dalam tipe kembalian templat adalah pola yang umum dan berguna (dengan konstruktor salinan yang baik, Anda dapat membuat fungsi templat yang efisien yang dapat diubah menjadi std::functiontipe yang dihapus jika Anda perlu segera menyimpannya di luar disebut konteks).
Yakk - Adam Nevraumont
1
@tohecz Saya sekarang menyebutkan apakah itu membutuhkan C ++ 11 atau tidak.
leemes
1
@Yakk Oh tentu saja, lupakan itu! Menambahkannya, terima kasih.
leemes
1
@MooingDuck Tentu saja tergantung pada implementasinya. Tetapi jika saya ingat dengan benar, karena cara kerja penghapusan tipe, ada satu lagi tipuan yang terjadi? Tapi sekarang saya memikirkannya lagi, saya rasa ini tidak terjadi jika Anda menetapkan pointer fungsi atau
lambda tanpa
1
@leemes: Benar, untuk function pointers atau captureless lambda, itu harus memiliki overhead yang sama dengan c-func-ptr. Yang masih warung pipa + tidak sebaris sepele.
Mooing Duck
26

void (*callbackFunc)(int); mungkin fungsi panggilan balik gaya C, tetapi fungsi ini sangat tidak dapat digunakan dengan desain yang buruk.

Callback gaya C yang dirancang dengan baik terlihat seperti void (*callbackFunc)(void*, int);- ia memiliki izin void*untuk memungkinkan kode yang melakukan callback mempertahankan status di luar fungsi. Tidak melakukan ini akan memaksa pemanggil untuk menyimpan status secara global, yang tidak sopan.

std::function< int(int) >akhirnya menjadi sedikit lebih mahal daripada int(*)(void*, int)pemanggilan di sebagian besar implementasi. Namun, lebih sulit bagi beberapa kompiler untuk melakukan inline. Ada std::functionimplementasi klon yang menyaingi overhead pemanggilan penunjuk fungsi (lihat 'delegasi tercepat yang mungkin', dll.) Yang mungkin masuk ke perpustakaan.

Sekarang, klien sistem callback sering kali perlu menyiapkan sumber daya dan membuangnya saat callback dibuat dan dihapus, dan untuk mengetahui masa pakai callback. void(*callback)(void*, int)tidak menyediakan ini.

Terkadang ini tersedia melalui struktur kode (masa pakai callback terbatas) atau melalui mekanisme lain (batalkan pendaftaran callback dan sejenisnya).

std::function menyediakan sarana untuk manajemen seumur hidup terbatas (salinan terakhir dari objek akan hilang jika dilupakan).

Secara umum, saya akan menggunakan std::functionkecuali masalah kinerja nyata. Jika ya, pertama-tama saya akan mencari perubahan struktural (alih-alih callback per-piksel, bagaimana dengan membuat prosesor scanline berdasarkan lambda yang Anda berikan kepada saya? Yang seharusnya cukup untuk mengurangi overhead panggilan fungsi ke tingkat yang sepele. ). Kemudian, jika tetap ada, saya akan menulis delegateberdasarkan kemungkinan delegasi tercepat, dan melihat apakah masalah kinerja hilang.

Saya kebanyakan hanya akan menggunakan pointer fungsi untuk API lama, atau untuk membuat antarmuka C untuk berkomunikasi antara kode yang dihasilkan kompiler berbeda. Saya juga telah menggunakannya sebagai detail implementasi internal ketika saya mengimplementasikan tabel lompat, penghapusan tipe, dll: ketika saya memproduksi dan mengkonsumsinya, dan saya tidak mengeksposnya secara eksternal untuk kode klien apa pun untuk digunakan, dan penunjuk fungsi melakukan semua yang saya butuhkan .

Perhatikan bahwa Anda bisa menulis pembungkus yang mengubah a std::function<int(int)>menjadi int(void*,int)gaya callback, dengan asumsi ada infrastruktur pengelolaan masa pakai callback yang tepat. Jadi sebagai tes asap untuk sistem manajemen seumur hidup callback C-style, saya akan memastikan bahwa membungkus std::functionbekerja dengan cukup baik.

Yakk - Adam Nevraumont
sumber
1
Dari mana void*asalnya ini ? Mengapa Anda ingin mempertahankan status di luar fungsi? Suatu fungsi harus berisi semua kode yang dibutuhkannya, semua fungsionalitas, Anda cukup meneruskannya ke argumen yang diinginkan dan memodifikasi dan mengembalikan sesuatu. Jika Anda memerlukan beberapa status eksternal lalu mengapa functionPtr atau callback membawa bagasi itu? Saya pikir panggilan balik itu tidak perlu rumit.
Nikos
@ nik-lz Saya tidak yakin bagaimana saya akan mengajari Anda penggunaan dan sejarah callback di C dalam komentar. Atau filosofi prosedural sebagai lawan dari pemrograman fungsional. Jadi, Anda akan membiarkannya tidak terpenuhi.
Yakk - Adam Nevraumont
Aku lupa this. Apakah karena seseorang harus memperhitungkan kasus fungsi anggota yang dipanggil, jadi kita memerlukan thispenunjuk untuk menunjuk ke alamat objek? Jika saya salah, bisakah Anda memberi saya tautan ke tempat saya dapat menemukan info lebih lanjut tentang ini, karena saya tidak dapat menemukan banyak tentang itu. Terima kasih sebelumnya.
Nikos
@ Fungsi anggota Nik-Lz bukanlah fungsi. Fungsi tidak memiliki status (waktu proses). Callback mengambil void*untuk mengizinkan transmisi status runtime. Sebuah fungsi pointer dengan void*dan void*argumen dapat meniru fungsi anggota panggilan untuk sebuah objek. Maaf, saya tidak tahu tentang resource yang menjalankan "mendesain mekanisme callback C 101".
Yakk - Adam Nevraumont
Ya, itulah yang saya bicarakan. Status waktu proses pada dasarnya adalah alamat dari objek yang dipanggil (karena berubah di antara proses). Ini masih tentang this. Itu yang saya maksud. Ok, terima kasih.
Nikos
17

Gunakan std::functionuntuk menyimpan objek callable arbitrer. Ini memungkinkan pengguna untuk memberikan konteks apa pun yang diperlukan untuk callback; pointer fungsi biasa tidak.

Jika Anda memang perlu menggunakan pointer fungsi biasa karena suatu alasan (mungkin karena Anda menginginkan API yang kompatibel dengan C), Anda harus menambahkan void * user_contextargumen sehingga setidaknya memungkinkan (meskipun tidak nyaman) untuk mengakses status yang tidak langsung diteruskan ke fungsi.

Mike Seymour
sumber
Apa jenis p di sini? akankah itu menjadi tipe std :: function? batal f () {}; otomatis p = f; p ();
sree
14

Satu-satunya alasan yang harus dihindari std::functionadalah dukungan dari compiler lama yang tidak memiliki dukungan untuk template ini, yang telah diperkenalkan di C ++ 11.

Jika mendukung bahasa pra-C ++ 11 tidak diwajibkan, penggunaan akan std::functionmemberi penelepon Anda lebih banyak pilihan dalam mengimplementasikan callback, menjadikannya opsi yang lebih baik dibandingkan dengan pointer fungsi "biasa". Ini menawarkan lebih banyak pilihan kepada pengguna API Anda, sambil mengabstraksi penerapannya secara spesifik untuk kode Anda yang melakukan callback.

Sergey Kalinichenko
sumber
1

std::function mungkin membawa VMT ke kode dalam beberapa kasus, yang berdampak pada kinerja.

vladon
sumber
3
Bisakah Anda menjelaskan apa VMT ini?
Gupta