Ketika mengembangkan dalam C ++, Anda harus menulis kode dari waktu ke waktu di mana pengecualian tidak boleh terjadi. Sebagai contoh, ketika kita perlu menulis swap bebas pengecualian untuk tipe asli atau mendefinisikan pernyataan pemindahan noexcept untuk kelas kita, atau secara manual mengimplementasikan destruktor nontrivial.
Dalam C ++ 11, pengubah noexcept ditambahkan ke bahasa, yang memungkinkan pengembang untuk memahami bahwa pengecualian tidak dapat dibuang dari fungsi yang ditandai dengan noexcept. Oleh karena itu, fungsi dengan tanda seperti itu dapat digunakan dengan aman dalam konteks di mana pengecualian tidak boleh muncul.
Misalnya, jika saya memiliki jenis dan fungsi ini:
class first_resource {...}; class second_resource {...}; void release(first_resource & r) noexcept; void close(second_resource & r);
dan ada kelas resources_owner
tertentu yang memiliki objek seperti first_resource
dan second_resource
:
class resources_owner { first_resource first_resource_; second_resource second_resource_; ... };
maka saya dapat menulis destructor resources_owner
sebagai berikut:
resources_owner::~resources_owner() noexcept {
Di satu sisi, kecuali di C ++ 11 membuat kehidupan seorang pengembang C ++ lebih mudah. Tetapi implementasi noexcept saat ini di C ++ modern memiliki satu sisi buruk ...
Kompiler tidak membantu mengontrol isi fungsi dan metode noexcept
Misalkan dalam contoh di atas saya salah: untuk beberapa alasan, saya menganggap release()
ditandai sebagai noexcept, tetapi pada kenyataannya itu tidak dan dapat melempar pengecualian. Ini berarti bahwa ketika saya menulis destruktor menggunakan release()
pelemparan release()
:
resources_owner::~resources_owner() noexcept { release(first_resource_);
maka saya mohon untuk masalah. Cepat atau lambat, release()
ini release()
akan melempar pengecualian dan seluruh aplikasi saya akan macet karena secara otomatis disebut std::terminate()
. Akan lebih buruk jika tidak crash aplikasi saya, tetapi orang lain, di mana mereka menggunakan perpustakaan saya dengan destructor yang bermasalah untuk resources_owner
.
Atau variasi lain dari masalah yang sama. Misalkan saya tidak salah bahwa release()
memang ditandai sebagai tidak kecuali. Itu tadi.
Itu ditandai dalam versi 1.0 dari perpustakaan pihak ketiga dari mana saya mengambil first_resource
dan release()
. Dan kemudian, setelah beberapa tahun, saya memutakhirkan ke versi 3.0 dari perpustakaan ini, tetapi dalam release()
versi 3.0 release()
tidak lagi memiliki pengubah noexcept.
Baiklah apa? Versi utama baru, mereka dapat dengan mudah memecahkan API.
Hanya sekarang, kemungkinan besar, saya akan lupa untuk memperbaiki implementasi destructor resources_owner
. Dan jika alih-alih saya orang lain terlibat dalam dukungan resource_owner
, yang tidak pernah melihat ke destruktor ini, maka perubahan dalam release()
tanda tangan mungkin tidak akan diketahui.
Oleh karena itu, saya pribadi tidak suka fakta bahwa kompiler tidak memperingatkan programmer dengan cara apa pun sehingga programmer di dalam metode / fungsi noexcept membuat panggilan metode / fungsi pengecualian.
Akan lebih baik jika kompiler akan mengeluarkan peringatan seperti itu.
Penyelamatan tenggelam adalah karya dari diri mereka yang tenggelam
OK, kompiler tidak memberikan peringatan apa pun. Dan tidak ada yang dapat dilakukan tentang pengembang sederhana ini. Jangan berurusan dengan modifikasi kompiler C ++ untuk kebutuhan Anda sendiri. Terutama jika Anda harus menggunakan bukan satu kompiler, tetapi versi berbeda dari kompiler C ++ berbeda.
Apakah mungkin untuk mendapatkan bantuan dari kompiler tanpa masuk ke jeroan ayam itiknya? Yaitu Apakah mungkin untuk membuat semacam alat untuk mengontrol isi metode / fungsi noexcept, bahkan jika metode dendro-fecal?
Kamu bisa. Ceroboh, tapi mungkin.
Dari mana kaki tumbuh?
Pendekatan yang dijelaskan dalam artikel ini diuji dalam praktik ketika menyiapkan versi berikutnya dari server HTTP kecil kami yang tertanam RESTinio .
Faktanya adalah karena RESTinio dipenuhi dengan fungsionalitas, kami telah kehilangan pandangan tentang masalah keselamatan pengecualian di beberapa tempat. Secara khusus, dari waktu ke waktu menjadi jelas bahwa pengecualian kadang-kadang dapat terbang keluar dari panggilan balik yang dikirim ke Asio (yang tidak seharusnya), serta pengecualian, pada prinsipnya, dapat terbang keluar saat membersihkan sumber daya.
Untungnya, dalam praktiknya, masalah-masalah ini tidak pernah terwujud, tetapi hutang teknis telah menumpuk dan sesuatu harus dilakukan untuk mengatasinya. Dan Anda harus melakukan sesuatu dengan kode yang sudah ditulis. Yaitu kode non-noexcept yang berfungsi harus dikonversikan menjadi kode noexcept yang berfungsi.
Ini dilakukan dengan bantuan beberapa makro, yang diatur oleh kode di tempat yang tepat. Misalnya, kasus sepele:
template< typename Message_Builder > void trigger_error_and_close( Message_Builder msg_builder ) noexcept {
Dan ini adalah fragmen yang tidak terlalu sepele:
void reset() noexcept { RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.empty()); RESTINIO_STATIC_ASSERT_NOEXCEPT( m_context_table.pop_response_context_nonchecked()); RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.front()); RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.front().dequeue_group()); RESTINIO_STATIC_ASSERT_NOEXCEPT(make_asio_compaible_error( asio_convertible_error_t::write_was_not_executed)); for(; !m_context_table.empty(); m_context_table.pop_response_context_nonchecked() ) { const auto ec = make_asio_compaible_error( asio_convertible_error_t::write_was_not_executed ); auto & current_ctx = m_context_table.front(); while( !current_ctx.empty() ) { auto wg = current_ctx.dequeue_group(); restinio::utils::suppress_exceptions_quietly( [&] { wg.invoke_after_write_notificator_if_exists( ec ); } ); } } }
Menggunakan makro ini berjabat tangan beberapa kali, menunjuk ke tempat-tempat yang secara tidak sengaja saya anggap sebagai tidak ada kecuali, tetapi ternyata tidak.
Jadi pendekatan yang diuraikan di bawah ini, tentu saja, adalah lisoped buatan sendiri dengan roda persegi, tapi itu berjalan ... Maksud saya berhasil.
Lebih lanjut dalam artikel ini, kita akan membahas implementasi yang diisolasi dari kode RESTinio ke dalam set makro yang terpisah.
Inti dari pendekatan
Inti dari pendekatan ini adalah meneruskan pernyataan / operator (stmt), yang perlu diperiksa untuk noexcept, ke dalam makro tertentu. Makro ini menggunakan static_assert(noexcept(stmt), msg)
untuk memverifikasi bahwa stmt benar-benar noexcept, dan kemudian mengganti stmt dalam kode.
Pada dasarnya, ini adalah:
ENSURE_NOEXCEPT_STATEMENT(release(some_resource));
akan diganti dengan sesuatu seperti:
static_assert(noexcept(release(some_resource)), "release(some_resource) is expected to be noexcept"); release(some_resource);
Mengapa pilihan dibuat demi makro dibuat?
Pada prinsipnya, orang dapat melakukannya tanpa makro dan orang dapat menulis static_assert(noexcept(...))
tepat di kode segera sebelum tindakan diperiksa. Tetapi macro memiliki setidaknya beberapa kebajikan yang memberi nilai pada skala yang mendukung penggunaan macro secara spesifik.
Pertama, makro mengurangi duplikasi kode. Ada perbandingan:
static_assert(noexcept(release(some_resource)), "release(some_resource) is expected to be noexcept"); release(some_resource);
dan
ENSURE_NOEXCEPT_STATEMENT(release(some_resource));
jelas bahwa dengan makro ekspresi utama, yaitu release(some_resource)
hanya dapat ditulis satu kali. Ini mengurangi kemungkinan kode "merayap" dari waktu ke waktu, dengan disertai, ketika koreksi dilakukan di satu tempat dan dilupakan di tempat kedua.
Kedua, makro dan, karenanya, cek yang tersembunyi di belakangnya bisa sangat mudah dinonaktifkan. Katakanlah, jika kelimpahan static_assert-s mulai mempengaruhi kecepatan kompilasi (walaupun saya tidak melihat efek seperti itu). Atau, yang lebih penting, ketika memperbarui beberapa perpustakaan pihak ketiga, kesalahan kompilasi dari static_assert yang tersembunyi di balik makro dapat menaburkan langsung ke sungai. Makro yang dinonaktifkan untuk sementara dapat memungkinkan pembaruan kode dengan lancar, termasuk makro verifikasi berurutan pertama dalam satu file, kemudian pada yang kedua, kemudian pada yang ketiga, dll.
Jadi makro, meskipun mereka adalah fitur yang ketinggalan zaman dan sangat kontroversial dalam C ++, dalam kasus khusus ini, kehidupan pengembang disederhanakan.
Makro utama ENSURE_NOEXCEPTPT_STATEMENT
ENSURE_NOEXCEPTPT_STATEMENT makro utama diimplementasikan sepele:
#define ENSURE_NOEXCEPT_STATEMENT(stmt) \ do { \ static_assert(noexcept(stmt), "this statement is expected to be noexcept: " #stmt); \ stmt; \ } while(false)
Ini digunakan untuk memverifikasi bahwa metode / fungsi yang dipanggil memang bukan pengecualian dan bahwa panggilan mereka tidak perlu dibingkai oleh blok try-catch. Sebagai contoh:
class some_complex_container { one_container first_data_part_; another_container second_data_part_; ... public: friend void swap(some_complex_container & a, some_complex_container & b) noexcept { using std::swap;
Selain itu, ada juga makro ENSURE_NOT_NOEXCEPT_STATEMENT. Ini digunakan untuk memastikan bahwa blok try-catch tambahan diperlukan di sekitar panggilan sehingga kemungkinan pengecualian tidak terbang keluar:
class some_resource_owner { some_resource resource_; ... public: ~some_resource_owner() noexcept { try {
Makro pembantu STATIC_ASSERT_NOEXCEPT dan STATIC_ASSERT_NOT_NOEXCEPT
Sayangnya, makro ENSURE_NOEXCEPT_STATEMENT dan ENSURE_NOT_NOEXCEPT_STATEMENT hanya dapat digunakan untuk pernyataan / pernyataan, tetapi tidak untuk ekspresi yang mengembalikan nilai. Yaitu Anda tidak dapat menulis dengan ENSURE_NOEXCEPT_STATEMENT seperti ini:
auto resource = ENSURE_NOEXCEPT_STATEMENT(acquire_resource(params));
Oleh karena itu, ENSURE_NOEXCEPTPT_STATEMENT tidak dapat digunakan, misalnya, dalam loop di mana Anda sering harus menulis sesuatu seperti:
for(auto i = something.get_first(); i != some_other_object; i = i.get_next()) {...}
dan Anda perlu memastikan bahwa get_first()
panggilan get_first()
, get_next()
, serta penugasan nilai-nilai baru untuk saya tidak membuang pengecualian.
Untuk mengatasi situasi seperti itu, makro STATIC_ASSERT_NOEXCEPT dan STATIC_ASSERT_NOT_NOEXCEPT ditulis, di belakangnya hanya static_assert yang disembunyikan dan tidak lebih. Dengan menggunakan makro ini, saya dapat mencapai hasil yang saya butuhkan dalam beberapa cara (kompilasi fragmen khusus ini tidak diperiksa):
STATIC_ASSERT_NOEXCEPT(something.get_first()); STATIC_ASSERT_NOEXCEPT(something.get_first().get_next()); STATIC_ASSERT_NOEXCEPT(std::declval<decltype(something.get_first())>() = something.get_first().get_next()); for(auto i = something.get_first(); i != some_other_object; i = i.get_next()) {...}
Jelas, ini bukan solusi terbaik, karena itu mengarah pada duplikasi kode dan meningkatkan risiko "merayap" dengan pemeliharaan lebih lanjut. Tetapi sebagai langkah pertama, makro sederhana ini ternyata bermanfaat.
Pustaka Noexcept-ctcheck
Ketika saya membagikan pengalaman ini di blog saya dan di Facebook, saya menerima proposal untuk mengatur perkembangan di atas di perpustakaan yang terpisah. Yang dilakukan: github sekarang memiliki pustaka header-only mungil noexcept-compile-time-check (atau noexcept-ctcheck, jika Anda menghemat surat) . Jadi semua hal di atas dapat Anda ambil dan coba. Benar, nama-nama makro sedikit lebih panjang dari apa yang digunakan dalam artikel. Yaitu NOEXCEPT_CTCHECK_ENSURE_NOEXCEPT_STATEMENT alih-alih ENSURE_NOEXCEPTPT_STATEMENT.
Apa yang tidak masuk ke noexcept-ctcheck (belum?)
Ada keinginan untuk membuat makro ENSURE_NOEXCEPTPT_EXPRESSION, yang dapat digunakan seperti ini:
auto resource = ENSURE_NOEXCEPT_EXPRESSION(acquire_resource(params));
Dalam perkiraan pertama, dia mungkin terlihat seperti ini:
#define ENSURE_NOEXCEPT_EXPRESSION(expr) \ ([&]() noexcept -> decltype(auto) { \ static_assert(noexcept(expr), #expr " is expected to be noexcept"); \ return expr; \ }())
Tapi ada kecurigaan yang kabur bahwa ada beberapa jebakan yang belum saya pikirkan. Secara umum, tangan belum mencapai ENSURE_NOEXCEPT_EXPRESSION :(
Dan jika kamu bermimpi?
Mimpi lama saya adalah untuk mendapatkan blok noexcept di C ++ di mana kompiler itu sendiri memeriksa untuk melempar pengecualian dan mengeluarkan peringatan jika pengecualian dapat dilempar. Menurut saya ini akan membuatnya lebih mudah untuk menulis kode pengecualian-aman. Dan tidak hanya dalam kasus-kasus nyata yang disebutkan di atas (swap, move-operator, destructors). Misalnya, blok noexcept dapat membantu dalam situasi ini:
void modify_some_complex_data() {
Di sini, untuk kebenaran kode, sangat penting bahwa tindakan yang dilakukan di dalam blok noexcept tidak membuang pengecualian. Dan jika kompiler dapat melacak ini, maka ini akan menjadi bantuan serius bagi pengembang.
Tapi mungkin blok noexcept hanyalah kasus khusus dari masalah yang lebih umum. Yaitu: memeriksa harapan programmer bahwa beberapa blok kode memiliki properti tertentu. Baik itu tidak adanya pengecualian, tidak ada efek samping, tidak adanya rekursi, ras data, dll.
Refleksi pada topik ini beberapa tahun yang lalu mengarah pada gagasan menyiratkan dan mengharapkan atribut . Gagasan ini tidak lebih dari posting blog, karena sementara dia menjauh dari minat dan peluang saya saat ini. Tetapi tiba-tiba itu akan menarik bagi seseorang dan seseorang akan mendorong untuk menciptakan sesuatu yang lebih layak.
Kesimpulan
Pada artikel ini, saya mencoba untuk berbicara tentang pengalaman saya dalam menyederhanakan penulisan kode pengecualian-aman. Menggunakan makro, tentu saja, tidak membuat kode lebih indah dan ringkas. Tapi itu berhasil. Dan bahkan makro primitif seperti itu meningkatkan koefisien tidur nyenyak saya cukup signifikan. Jadi, jika orang lain belum memikirkan cara mengontrol konten metode / fungsi noexcept mereka sendiri, maka mungkin artikel ini akan menginspirasi Anda untuk memikirkan topik ini.
Dan jika seseorang menemukan cara untuk menyederhanakan hidup mereka ketika menulis kode noexcept, maka akan menarik untuk mengetahui apa metode ini, di mana itu membantu dan di mana itu tidak. Dan seberapa puas Anda dengan apa yang Anda gunakan.