Apakah mengkonversi metode C ++ ke fungsi C dengan argumen pointer merupakan pola yang dapat diterima?

16

Saya menggunakan C ++ pada ESP-32. Saat mendaftarkan timer saya harus melakukan ini:

timer_args.callback = reinterpret_cast<esp_timer_cb_t>(&SoundMixer::soundCallback);
timer_args.arg = this;

Di sini timer memanggil soundCallback.

Dan hal yang sama saat mendaftarkan tugas:

xTaskCreate(reinterpret_cast<TaskFunction_t>(&SoundProviderTask::taskProviderCode), "SProvTask", stackSize, this, 10, &taskHandle);

Jadi metode ini dimulai dalam tugas yang terpisah.

GCC selalu memperingatkan saya tentang konversi ini, tetapi itu berfungsi seperti yang direncanakan.

Apakah ini dapat diterima dalam kode produksi? Apakah ada cara yang lebih baik untuk melakukan ini?

val mengatakan Reinstate Monica
sumber

Jawaban:

47

A reinterpret_castselalu mencurigakan kecuali Anda tahu persis apa yang Anda lakukan. Di sini, kode Anda hanya berfungsi karena konvensi pemanggilan GCC untuk metode C ++, tapi ini baunya seperti perilaku yang tidak terdefinisi. Khususnya Anda tidak boleh berasumsi bahwa fungsi anggota dengan cara apa pun kompatibel dengan pointer fungsi normal.

Pendekatan yang biasa dilakukan adalah mendefinisikan fungsi yang kompatibel dengan C dengan tanda tangan yang sesuai, yang secara internal memanggil metode C ++. Sebagai contoh:

extern "C" static void my_timer_callback(void* arg) {
  static_cast<SoundMixer*>(arg)->soundCallback();
}

Gips ini baik-baik saja karena kita melakukan casting kembali dari a void*ke tipe objek berujung runcing.

Detail:

  • extern "C"menentukan keterkaitan bahasa dari fungsi ini. Keterkaitan bahasa memengaruhi susunan nama dan konvensi pemanggilan fungsi. Fungsi anggota tidak dapat memiliki tautan bahasa C. Keterkaitan bahasa sebagian besar ortogonal dengan keterkaitan internal / eksternal.

  • Untuk panggilan balik, fungsinya mungkin "pribadi", yaitu memiliki tautan internal. Kode C tidak pernah merujuk pada panggilan balik dengan nama. Cuplikan kode di atas menentukan hubungan internal melalui statickata kunci (bukan metode statis!). Atau, fungsi tersebut dapat ditempatkan ke namespace anonim.

    Saya tidak sepenuhnya yakin tentang interaksi antara extern "C"dan static(hubungan internal). Misalnya [dcl.link]mengatakan bahwa “Semua jenis fungsi, nama fungsi dengan hubungan eksternal, dan nama variabel dengan linkage eksternal memiliki hubungan bahasa.” Saya menafsirkan ini sehingga jenis dari my_timer_callbackmemiliki C linkage bahasa, tapi itu fungsinya nama tidak.

  • A static_castsesuai di sini karena kita tahu tipe nyata argtetapi tidak dapat mengungkapkannya dalam sistem tipe. Sebaliknya a reinterpret_castadalah tepat ketika kita ingin menafsirkan kembali pola bit, misalnya penunjuk ke tipe numerik.

  • Fungsi bukan objek biasa, dan fungsi anggota bahkan kurang begitu. Anda dapat menafsirkan ulang-melemparkan antara tipe-tipe pointer fungsi selama fungsi tersebut hanya dipanggil melalui tipe aslinya (dan analog dengan pointer fungsi anggota). Apakah Anda dapat melemparkan pointer fungsi ke tipe lain (mis. Pointer objek atau pointer kosong) ditentukan oleh implementasi ( latar belakang ). Pada POSIX, gulir di antara pointer fungsi dan void*diizinkan sehingga dlsym()dapat bekerja. Gips lain yang melibatkan pointer fungsi (anggota) tidak ditentukan. Secara khusus, gips antara fungsi anggota dan pointer fungsi tidak dimungkinkan.

amon
sumber
1
Tidak std::bindjuga menganggap penunjuk objek sebagai argumen metode pertama?
val berkata Reinstate Monica
5
@val Ya, tetapi itu tidak berarti bahwa fungsi anggota kompatibel dengan fungsi biasa, hanya itu mengikat () menggunakan algoritma INVOKE yang menangani fungsi anggota sebagai kasus terpisah dari objek fungsi biasa termasuk. pointer fungsi. Karena std :: bind () membuat functor, itu tidak cocok untuk berinteraksi dengan C.
amon
1
Pertanyaan lain: mengapa saya perlu di extern "C"sini? Apakah hubungan C penting dalam kasus ini?
val berkata Reinstate Monica
5
@val Jika Anda ingin dapat memanggil fungsi itu dari C, ia harus menggunakan konvensi pemanggilan C. Ini dapat dilakukan dengan mendeklarasikan fungsi itu dengan pertautan bahasa C, atau dengan ekstensi khusus-kompiler (seperti __attribute__((cdecl)), tapi tolong jangan lakukan itu). Fungsi C ++ tidak dijamin memiliki konvensi panggilan yang kompatibel dengan C jika tidak (meskipun dalam GCC biasanya berfungsi dengan baik).
amon
4
@val Untuk perincian tentang mengapa extern "C"secara formal diperlukan, lihat [dcl.link]"Dua tipe fungsi dengan hubungan bahasa yang berbeda adalah tipe yang berbeda walaupun mereka identik." dan [expr.call]"Memanggil fungsi melalui ekspresi yang tipe fungsinya berbeda dari tipe fungsi dari definisi fungsi yang disebut menghasilkan perilaku yang tidak ditentukan"
Ben Voigt
-1

Secara pribadi, pendekatan yang paling kompatibel, mudah diimplementasikan dan mudah dipahami yang saya temukan adalah dengan hanya menyediakan fungsi "wrapper", kompatibel dengan antarmuka C yang diharapkan, yang secara internal memanggil metode (dan, jika itu tidak statis, instantiate atau gunakan instance yang ada untuk melakukan itu). Itu bisa dilihat sebagai semacam variasi dari Pola Desain Adaptor.

Jesus Alonso Abad
sumber
6
Bukankah itu yang dijawab amon?
Dronz
1
@Ronz setelah membaca kedua, ya, sebagian besar itu. Begitu saya membaca staticsaya melihatnya sebagai metode dan untuk beberapa alasan tidak menyadari itu tidak melewati thispointer sebagai argumen pertama (dan perdebatan berikut tentang penggunaan std::binddiperkuat). Tapi ya, Anda memang benar! (Maaf untuk jawaban ganda!)
Jesus Alonso Abad
3
Ya, staticmemiliki setidaknya tiga makna berbeda. Dan Anda akan mencampurnya jika Anda tidak hati-hati. Saya akan mengatakan, sangat membantu untuk memahami perbedaan antara penggunaan yang berbeda static, karena masing-masing adalah alat yang hebat dalam dirinya sendiri.
cmaster - mengembalikan monica