Katakanlah saya memiliki sebuah fungsi yang menerima sebuah void (*)(void*)
function pointer untuk digunakan sebagai callback:
void do_stuff(void (*callback_fp)(void*), void* callback_arg);
Sekarang, jika saya memiliki fungsi seperti ini:
void my_callback_function(struct my_struct* arg);
Bisakah saya melakukan ini dengan aman?
do_stuff((void (*)(void*)) &my_callback_function, NULL);
Saya telah melihat pertanyaan ini dan saya telah melihat beberapa standar C yang mengatakan Anda dapat mentransmisikan ke 'penunjuk fungsi yang kompatibel', tetapi saya tidak dapat menemukan definisi tentang apa arti 'penunjuk fungsi yang kompatibel'.
c
function-pointers
Mike Weller
sumber
sumber
void (*func)(void *)
artinyafunc
adalah penunjuk ke fungsi dengan tanda tangan tipe sepertivoid foo(void *arg)
. Jadi ya, kamu benar.Jawaban:
Sejauh menyangkut standar C, jika Anda mentransmisikan penunjuk fungsi ke penunjuk fungsi dari tipe yang berbeda dan kemudian memanggilnya, itu adalah perilaku yang tidak ditentukan . Lihat Lampiran J.2 (informatif):
Bagian 6.3.2.3, paragraf 8 berbunyi:
Jadi dengan kata lain, Anda dapat mentransmisikan penunjuk fungsi ke jenis penunjuk fungsi yang berbeda, melemparkannya kembali, dan memanggilnya, dan semuanya akan berfungsi.
Definisi kompatibel agak rumit. Ini dapat ditemukan di bagian 6.7.5.3, paragraf 15:
Aturan untuk menentukan apakah dua jenis kompatibel dijelaskan di bagian 6.2.7, dan saya tidak akan mengutipnya di sini karena agak panjang, tetapi Anda dapat membacanya di draf standar C99 (PDF) .
Aturan yang relevan di sini ada di bagian 6.7.5.1, paragraf 2:
Oleh karena itu, karena a
void*
tidak kompatibel dengan astruct my_struct*
, penunjuk fungsi bertipevoid (*)(void*)
tidak kompatibel dengan penunjuk fungsi jenisvoid (*)(struct my_struct*)
, jadi casting penunjuk fungsi ini secara teknis merupakan perilaku yang tidak terdefinisi.Dalam praktiknya, Anda dapat dengan aman menggunakan pointer fungsi casting dalam beberapa kasus. Dalam konvensi pemanggilan x86, argumen didorong pada stack, dan semua pointer memiliki ukuran yang sama (4 byte dalam x86 atau 8 byte dalam x86_64). Memanggil pointer fungsi bermuara pada mendorong argumen pada stack dan melakukan lompatan tidak langsung ke target pointer fungsi, dan jelas tidak ada gagasan tentang tipe pada level kode mesin.
Hal-hal yang pasti tidak bisa Anda lakukan:
stdcall
konvensi pemanggilan (yang macroCALLBACK
,PASCAL
danWINAPI
semua memperluas untuk). Jika Anda meneruskan penunjuk fungsi yang menggunakan konvensi panggilan C standar (cdecl
), hal buruk akan terjadi.this
parameter tersembunyi , dan jika Anda mentransmisikan fungsi anggota ke fungsi biasa, tidak adathis
objek untuk digunakan, dan sekali lagi, banyak kejahatan akan terjadi.Ide buruk lainnya yang terkadang berhasil tetapi juga merupakan perilaku yang tidak terdefinisi:
void (*)(void)
ke avoid*
). Pointer fungsi belum tentu berukuran sama dengan pointer biasa, karena pada beberapa arsitektur mungkin terdapat informasi kontekstual tambahan. Ini mungkin akan bekerja dengan baik pada x86, tetapi ingatlah bahwa ini adalah perilaku yang tidak ditentukan.sumber
void*
adalah bahwa mereka kompatibel dengan penunjuk lain? Seharusnya tidak ada masalah mentransmisikan astruct my_struct*
kevoid*
, pada kenyataannya Anda bahkan tidak perlu melakukan transmisi, kompilator harus menerimanya. Misalnya, jika Anda meneruskan astruct my_struct*
ke fungsi yang membutuhkanvoid*
, tidak perlu casting. Apa yang saya lewatkan di sini yang membuat ini tidak kompatibel?void*
seperti untuk jenis penunjuk fungsi, lihat spesifikasi .void *
hanya "kompatibel dengan" penunjuk (non-fungsi) lain dengan cara yang didefinisikan dengan sangat tepat (yang tidak terkait dengan apa yang dimaksud standar C dengan kata "kompatibel" dalam kasus ini). C memungkinkan avoid *
menjadi lebih besar atau lebih kecil dari astruct my_struct *
, atau memiliki bit dalam urutan yang berbeda atau dinegasikan atau apa pun. Jadivoid f(void *)
danvoid f(struct my_struct *)
bisa jadi tidak kompatibel dengan ABI . C akan mengonversi pointer itu sendiri untuk Anda jika diperlukan, tetapi C tidak akan dan terkadang tidak dapat mengubah fungsi menunjuk ke untuk mengambil jenis argumen yang mungkin berbeda.Saya bertanya tentang masalah yang sama persis mengenai beberapa kode di GLib baru-baru ini. (GLib adalah pustaka inti untuk proyek GNOME dan ditulis dalam C.) Saya diberitahu bahwa seluruh kerangka kerja sinyal slot bergantung padanya.
Di seluruh kode, ada banyak contoh casting dari tipe (1) hingga (2):
typedef int (*CompareFunc) (const void *a, const void *b)
typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)
Biasanya melakukan chain-thru dengan panggilan seperti ini:
int stuff_equal (GStuff *a, GStuff *b, CompareFunc compare_func) { return stuff_equal_with_data(a, b, (CompareDataFunc) compare_func, NULL); } int stuff_equal_with_data (GStuff *a, GStuff *b, CompareDataFunc compare_func, void *user_data) { int result; /* do some work here */ result = compare_func (data1, data2, user_data); return result; }
Lihat sendiri di sini di
g_array_sort()
: http://git.gnome.org/browse/glib/tree/glib/garray.cJawaban di atas rinci dan kemungkinan besar benar - jika Anda duduk di komite standar. Adam dan Johannes berhak mendapatkan pujian atas tanggapan mereka yang telah diteliti dengan baik. Namun, di alam liar, Anda akan menemukan kode ini berfungsi dengan baik. Kontroversial? Iya. Pertimbangkan ini: GLib mengkompilasi / bekerja / menguji pada sejumlah besar platform (Linux / Solaris / Windows / OS X) dengan berbagai macam kompiler / penaut / pemuat kernel (GCC / CLang / MSVC). Standar terkutuk, kurasa.
Saya menghabiskan beberapa waktu untuk memikirkan jawaban-jawaban ini. Inilah kesimpulan saya:
Berpikir lebih dalam setelah menulis tanggapan ini, saya tidak akan terkejut jika kode untuk kompiler C menggunakan trik yang sama. Dan karena (hampir semua?) Kompiler C modern di-bootstrap, ini berarti triknya aman.
Pertanyaan yang lebih penting untuk diteliti: Dapatkah seseorang menemukan platform / compiler / linker / loader di mana trik ini tidak berfungsi? Poin brownies utama untuk yang satu itu. Saya yakin ada beberapa prosesor / sistem tertanam yang tidak menyukainya. Namun, untuk komputasi desktop (dan mungkin seluler / tablet), trik ini mungkin masih berfungsi.
sumber
Intinya bukanlah apakah Anda bisa. Solusi sepele adalah
void my_callback_function(struct my_struct* arg); void my_callback_helper(void* pv) { my_callback_function((struct my_struct*)pv); } do_stuff(&my_callback_helper);
Kompiler yang baik hanya akan menghasilkan kode untuk my_callback_helper jika benar-benar dibutuhkan, dalam hal ini Anda akan senang melakukannya.
sumber
my_callback_helper
, kecuali selalu inline. Ini jelas tidak perlu, karena satu-satunya hal yang cenderung dilakukannya adalahjmp my_callback_function
. Kompilator mungkin ingin memastikan alamat untuk fungsi berbeda, tetapi sayangnya ia melakukan ini bahkan ketika fungsinya ditandai dengan C99inline
(yaitu "tidak peduli tentang alamatnya").void *
bisa berukuran berbeda dari astruct *
(saya pikir itu salah, karena jikamalloc
tidak akan rusak, tetapi komentar itu memiliki 5 suara positif, jadi saya memberinya kredit. Jika @mtraceur benar, solusi yang Anda tulis tidak akan benar.void*
masih harus berhasil. Singkatnya,void*
mungkin memiliki lebih banyak bit, tetapi jika Anda mentransmisikanstruct*
kevoid*
bit ekstra tersebut dapat menjadi nol dan membuang kembali hanya dapat membuang nol itu lagi.void *
bisa (secara teori) begitu berbeda dari astruct *
. Saya mengimplementasikan vtable di C, dan saya menggunakanthis
pointer C ++ - ish sebagai argumen pertama untuk fungsi virtual. Jelas,this
harus menjadi penunjuk ke struct "saat ini" (diturunkan). Jadi, fungsi virtual membutuhkan prototipe yang berbeda tergantung pada struct tempat mereka diimplementasikan. Saya pikir menggunakanvoid *this
argumen akan memperbaiki semuanya tapi sekarang saya belajar itu adalah perilaku yang tidak ditentukan ...Anda memiliki tipe fungsi yang kompatibel jika tipe kembalian dan tipe parameternya kompatibel - pada dasarnya (kenyataannya lebih rumit :)). Kompatibilitas sama dengan "tipe yang sama" hanya saja lebih longgar untuk memungkinkan memiliki tipe yang berbeda tetapi masih memiliki beberapa bentuk yang mengatakan "tipe ini hampir sama". Di C89, misalnya, dua struct kompatibel jika keduanya identik tetapi hanya namanya yang berbeda. C99 tampaknya telah mengubah itu. Mengutip dari dokumen rasional (bacaan yang sangat dianjurkan, btw!):
Yang mengatakan - ya, ini benar-benar perilaku yang tidak terdefinisi, karena fungsi do_stuff Anda atau orang lain akan memanggil fungsi Anda dengan penunjuk fungsi yang memiliki
void*
parameter, tetapi fungsi Anda memiliki parameter yang tidak kompatibel. Namun demikian, saya mengharapkan semua kompiler untuk mengkompilasi dan menjalankannya tanpa mengeluh. Tetapi Anda dapat melakukan pembersihan dengan meminta fungsi lain mengambilvoid*
(dan mendaftarkannya sebagai fungsi panggilan balik) yang kemudian akan memanggil fungsi Anda yang sebenarnya.sumber
Karena kode C mengkompilasi ke instruksi yang sama sekali tidak peduli tentang jenis penunjuk, cukup baik untuk menggunakan kode yang Anda sebutkan. Anda akan mengalami masalah ketika Anda menjalankan do_stuff dengan fungsi callback dan penunjuk ke sesuatu yang lain lalu struktur my_struct sebagai argumen.
Saya harap saya bisa membuatnya lebih jelas dengan menunjukkan apa yang tidak berhasil:
int my_number = 14; do_stuff((void (*)(void*)) &my_callback_function, &my_number); // my_callback_function will try to access int as struct my_struct // and go nuts
atau...
void another_callback_function(struct my_struct* arg, int arg2) { something } do_stuff((void (*)(void*)) &another_callback_function, NULL); // another_callback_function will look for non-existing second argument // on the stack and go nuts
Pada dasarnya, Anda dapat mentransmisikan petunjuk ke apa pun yang Anda suka, selama data tetap masuk akal pada saat proses dijalankan.
sumber
Jika Anda berpikir tentang cara kerja pemanggilan fungsi di C / C ++, pemanggilan fungsi tersebut mendorong item tertentu di tumpukan, melompat ke lokasi kode baru, mengeksekusi, lalu memunculkan tumpukan saat kembali. Jika pointer fungsi Anda mendeskripsikan fungsi dengan tipe kembalian yang sama dan jumlah / ukuran argumen yang sama, Anda akan baik-baik saja.
Jadi, saya pikir Anda harus bisa melakukannya dengan aman.
sumber
struct
-pointers danvoid
-pointers memiliki representasi bit yang kompatibel; itu tidak dijamin akan terjadiVoid pointer kompatibel dengan jenis pointer lainnya. Ini adalah tulang punggung bagaimana malloc dan fungsi mem (
memcpy
,memcmp
) bekerja. Biasanya, di C (Bukan C ++)NULL
makro didefinisikan sebagai((void *)0)
.Lihat 6.3.2.3 (Item 1) di C99:
sumber