Penanganan kesalahan dengan C++/WinRT

Topik ini membahas strategi untuk menangani kesalahan saat pemrograman dengan C++/WinRT. Untuk informasi umum selengkapnya, dan latar belakang, lihat Penanganan Kesalahan dan Pengecualian (Modern C++).

Hindari menangkap dan melemparkan pengecualian

Kami menyarankan agar Anda terus menulis kode yang aman terhadap pengecualian, tetapi mengutamakan untuk menghindari menangkap dan melempar pengecualian jika memungkinkan. Jika tidak ada penangan untuk eksepsi, Windows akan secara otomatis membuat laporan kesalahan (termasuk minidump saat crash), yang akan membantu Anda melacak sumber masalahnya.

Jangan melempar pengecualian yang Anda harapkan akan tertangkap. Dan jangan gunakan pengecualian untuk kegagalan yang diharapkan. Lempar pengecualian hanya saat terjadi galat runtime yang tidak terduga, dan tangani semua hal lainnya dengan kode galat/hasil—secara langsung, dan sedekat mungkin dengan sumber kegagalan. Dengan begitu, ketika pengecualian dilemparkan , Anda tahu bahwa penyebabnya adalah bug dalam kode Anda, atau status kesalahan yang luar biasa dalam sistem.

Pertimbangkan skenario mengakses Registri Windows. Jika aplikasi Anda gagal membaca nilai dari Registri, maka itu yang diharapkan, dan Anda harus menanganinya dengan baik. Jangan memunculkan pengecualian; sebaliknya, kembalikan nilai bool atau enum yang menunjukkan bahwa nilai tersebut tidak terbaca, dan mungkin juga alasannya. Di sisi lain, kegagalan menulis nilai ke Registry kemungkinan besar menandakan bahwa ada masalah yang lebih besar daripada yang dapat Anda tangani secara wajar dalam aplikasi Anda. Dalam kasus seperti itu, Anda tidak ingin aplikasi Anda berlanjut, jadi pengecualian yang menghasilkan laporan kesalahan adalah cara tercepat untuk menjaga aplikasi Anda tidak menyebabkan bahaya.

Misalnya, pertimbangkan untuk mengambil gambar mini dari panggilan ke StorageFile.GetThumbnailAsync, lalu meneruskan gambar mini tersebut ke BitmapSource.SetSourceAsync. Jika urutan panggilan tersebut menyebabkan Anda meneruskan nullptr ke SetSourceAsync (file gambar tidak dapat dibaca; mungkin ekstensi filenya membuatnya terlihat seperti berisi data gambar, tetapi sebenarnya tidak), maka Anda akan menyebabkan pengecualian pointer yang tidak valid dilemparkan. Jika Anda menemukan kasus seperti itu dalam kode Anda, alih-alih menangkap dan menanganinya sebagai pengecualian, periksa apakah nullptr dikembalikan dari GetThumbnailAsync.

Melemparkan pengecualian cenderung lebih lambat daripada menggunakan kode kesalahan. Jika Anda hanya memunculkan pengecualian saat terjadi kesalahan fatal, maka jika semuanya berjalan lancar, Anda tidak akan pernah menanggung penalti kinerja.

Namun, penurunan performa yang lebih mungkin berasal dari overhead runtime untuk memastikan bahwa destruktor yang tepat dipanggil dalam kejadian langka ketika pengecualian terjadi. Biaya jaminan ini tetap harus ditanggung, baik pengecualian benar-benar dilemparkan maupun tidak. Jadi, Anda harus memastikan bahwa pengkompilasi memiliki ide yang baik tentang fungsi apa yang berpotensi melemparkan pengecualian. Jika pengkompilasi dapat membuktikan bahwa tidak akan ada pengecualian dari fungsi tertentu ( noexcept spesifikasinya), maka ia dapat mengoptimalkan kode yang dihasilkannya.

Menangani eksepsi

Kondisi kesalahan yang muncul pada lapisan ABI Windows Runtime dikembalikan dalam bentuk nilai HRESULT. Tetapi Anda tidak perlu menangani HRESULT dalam kode Anda. Kode proyeksi C++/WinRT yang dihasilkan untuk API di sisi yang mengonsumsi mendeteksi kesalahan kode HRESULT di lapisan ABI dan mengonversi kode menjadi pengecualian winrt::hresult_error , yang dapat Anda tangkap dan tangani. Jika Anda ingin menangani HRESULT, maka gunakan jenis winrt::hresult .

Misalnya, jika pengguna kebetulan menghapus gambar dari Pustaka Gambar saat aplikasi Anda melakukan iterasi pada koleksi tersebut, proyeksi akan melemparkan pengecualian. Dan ini adalah kasus di mana Anda harus menangkap dan menangani pengecualian itu. Berikut adalah contoh kode yang menunjukkan kasus ini.

#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Microsoft.UI.Xaml.Media.Imaging.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Storage;
using namespace Microsoft::UI::Xaml::Media::Imaging;

IAsyncAction MakeThumbnailsAsync()
{
    auto imageFiles{ co_await KnownFolders::PicturesLibrary().GetFilesAsync() };

    for (StorageFile const& imageFile : imageFiles)
    {
        BitmapImage bitmapImage;
        try
        {
            auto thumbnail{ co_await imageFile.GetThumbnailAsync(FileProperties::ThumbnailMode::PicturesView) };
            if (thumbnail) bitmapImage.SetSource(thumbnail);
        }
        catch (winrt::hresult_error const& ex)
        {
            winrt::hresult hr = ex.code(); // HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND).
            winrt::hstring message = ex.message(); // The system cannot find the file specified.
        }
    }
}

Gunakan pola yang sama di dalam coroutine saat memanggil fungsi yang diberi co_await. Contoh lain dari konversi HRESULT menjadi exception ini terjadi ketika API komponen mengembalikan E_OUTOFMEMORY, yang akan memicu pelemparan std::bad_alloc.

Sebaiknya gunakan winrt::hresult_error::code jika Anda hanya ingin melihat kode HRESULT. Fungsi winrt::hresult_error::to_abi di sisi lain mengonversi ke objek kesalahan COM, dan mendorong status ke penyimpanan com thread-local.

Memunculkan pengecualian

Akan ada kasus di mana Anda memutuskan bahwa, jika panggilan Anda ke fungsi tertentu gagal, aplikasi Anda tidak akan dapat pulih (Anda tidak akan lagi dapat mengandalkannya untuk berfungsi secara terprediksi). Contoh kode di bawah ini menggunakan nilai winrt::handle sebagai pembungkus di sekitar HANDLE yang dikembalikan dari CreateEvent. Kemudian meneruskan handle tersebut (membuat nilai bool darinya) ke templat fungsi winrt::check_bool. winrt::check_bool bekerja dengan bool, atau dengan nilai apa pun yang dapat dikonversi ke false (kondisi kesalahan), atau true (kondisi keberhasilan).

winrt::handle h{ ::CreateEvent(nullptr, false, false, nullptr) };
winrt::check_bool(bool{ h });
winrt::check_bool(::SetEvent(h.get()));

Jika nilai yang Anda berikan ke winrt::check_bool salah, maka urutan tindakan berikut terjadi.

Karena API Windows melaporkan kesalahan run-time menggunakan berbagai jenis nilai pengembalian, ada selain winrt::check_bool beberapa fungsi pembantu berguna lainnya untuk memeriksa nilai dan melemparkan pengecualian.

  • winrt::check_hresult. Memeriksa apakah kode HRESULT mewakili kesalahan dan, jika demikian, memanggil winrt::throw_hresult.
  • winrt::check_nt. Memeriksa apakah kode mewakili kesalahan dan, jika demikian, memanggil winrt::throw_hresult.
  • winrt::check_pointer. Memeriksa apakah pointer null dan, jika demikian, memanggil winrt::throw_last_error.
  • winrt::check_win32. Memeriksa apakah kode mewakili kesalahan dan, jika demikian, memanggil winrt::throw_hresult.

Anda dapat menggunakan fungsi pembantu ini untuk jenis kode pengembalian umum, atau Anda dapat merespons kondisi kesalahan apa pun dan memanggil winrt::throw_last_error atau winrt::throw_hresult.

Melemparkan pengecualian saat menulis API

Semua batas Antarmuka Biner Aplikasi Windows Runtime (atau batas ABI) harus noexcept—artinya, eksepsi tidak boleh pernah melewati batas tersebut. Saat Anda menulis API, Anda harus selalu menandai batas ABI dengan kata kunci C++ noexcept . noexcept memiliki perilaku khusus di C++. Jika pengecualian C++ mencapai batas noexcept, proses akan langsung dihentikan dengan std::terminate. Perilaku itu umumnya diinginkan, karena pengecualian yang tidak tertangani hampir selalu menyiratkan status yang tidak diketahui dalam prosesnya.

Karena pengecualian tidak boleh melewati batas ABI, kondisi kesalahan yang muncul dalam implementasi dikembalikan di seluruh lapisan ABI dalam bentuk kode kesalahan HRESULT. Saat Anda menulis API menggunakan C++/WinRT, kode dibuat agar Anda dapat mengonversi pengecualian apa pun yang Anda lemparkan dalam implementasi Anda ke HRESULT. Fungsi winrt::to_hresult digunakan dalam kode yang dihasilkan dalam pola seperti ini.

HRESULT DoWork() noexcept
{
    try
    {
        // Shim through to your C++/WinRT implementation.
        return S_OK;
    }
    catch (...)
    {
        return winrt::to_hresult(); // Convert any exception to an HRESULT.
    }
}

winrt::to_hresult menangani pengecualian yang berasal dari std::exception, dan winrt::hresult_error dan jenis turunannya. Dalam implementasi Anda, Anda harus lebih memilih winrt::hresult_error, atau jenis turunan, sehingga konsumen API Anda menerima informasi kesalahan yang kaya. std::exception (yang dipetakan ke E_FAIL) didukung jika eksepsi timbul akibat penggunaan Standard Template Library oleh Anda.

Kemudahan penelusuran bug dengan noexcept

Seperti yang kami sebutkan di atas, eksepsi C++ yang mencapai batas noexcept akan langsung dihentikan dengan std::terminate. Itu tidak ideal untuk debugging, karena std::terminate sering kehilangan banyak atau semua kesalahan atau konteks pengecualian yang dilemparkan, terutama ketika coroutines terlibat.

Jadi, bagian ini berkaitan dengan kasus di mana metode ABI Anda (yang telah Anda anotasi dengan benar dengan noexcept) menggunakan co_await untuk memanggil kode proyeksi C++/WinRT asinkron. Sebaiknya Anda membungkus panggilan ke kode proyeksi C++/WinRT di dalam winrt::fire_and_forget. Dengan melakukan hal tersebut, tersedia tempat yang semestinya agar pengecualian yang tidak tertangani dapat dicatat dengan semestinya sebagai pengecualian yang disimpan, yang sangat meningkatkan kemudahan penelusuran kesalahan.

HRESULT MyWinRTObject::MyABI_Method() noexcept
{
    winrt::com_ptr<Foo> foo{ get_a_foo() };

    [/*no captures*/](winrt::com_ptr<Foo> foo) -> winrt::fire_and_forget
    {
        co_await winrt::resume_background();

        foo->ABICall();

        AnotherMethodWithLotsOfProjectionCalls();
    }(foo);

    return S_OK;
}

winrt::fire_and_forget memiliki pembantu metode bawaan unhandled_exception , yang memanggil winrt::terminate, yang pada gilirannya memanggil RoFailFastWithErrorContext. Hal ini memastikan bahwa segala konteks (pengecualian yang disimpan, kode kesalahan, pesan kesalahan, jejak balik tumpukan, dan sebagainya) tetap dipertahankan baik untuk penelusuran kesalahan secara langsung maupun untuk dump post-mortem. Untuk kenyamanan, Anda dapat memperhitungkan bagian fire-and-forget ke dalam fungsi terpisah yang mengembalikan winrt::fire_and_forget, lalu memanggilnya.

Kode sinkron

Dalam beberapa kasus, metode ABI Anda (yang sekali lagi, Anda telah membuat anotasi dengan benar dengan noexcept) hanya memanggil kode sinkron. Dengan kata lain, ia tidak pernah menggunakan co_await, baik untuk memanggil metode Windows Runtime asinkron, atau untuk beralih antara utas latar depan dan latar belakang. Dalam hal ini, teknik fire_and_forget masih akan berfungsi, tetapi tidak efisien. Sebagai gantinya, Anda dapat melakukan sesuatu seperti ini.

HRESULT abi() noexcept try
{
    // ABI code goes here.
} catch (...) { winrt::terminate(); }

Gagal sedini mungkin

Kode di bagian sebelumnya tetap segera gagal. Seperti yang ditulis, kode itu tidak menangani pengecualian apa pun. Setiap pengecualian yang tidak tertangani menghasilkan penghentian program.

Tetapi bentuk itu lebih unggul, karena memastikan debuggability. Dalam kasus langka, Anda mungkin ingin try/catch dan menangani pengecualian tertentu. Tetapi itu harus jarang terjadi karena, seperti yang dijelaskan topik ini, kami mencegah penggunaan pengecualian sebagai mekanisme kontrol aliran untuk kondisi yang Anda harapkan.

Ingatlah bahwa itu adalah ide yang buruk untuk membiarkan pengecualian yang tidak tertangani lolos dari konteks telanjang noexcept . Dalam kondisi tersebut, runtime C++ akan std::terminate proses, sehingga informasi pengecualian yang disimpan yang telah dicatat dengan cermat oleh C++/WinRT akan hilang.

Assertions

Untuk asumsi internal di aplikasi Anda, tersedia asersi. Pilih static_assert untuk validasi waktu kompilasi, sedapat mungkin. Untuk kondisi run-time, gunakan WINRT_ASSERT dengan ekspresi Boolean. WINRT_ASSERT adalah definisi makro, dan meluas ke _ASSERTE.

WINRT_ASSERT(pos < size());

WINRT_ASSERT dihilangkan saat kompilasi dalam build rilis; dalam build debug, ini akan menghentikan aplikasi di debugger pada baris kode tempat asersi tersebut berada.

Anda tidak boleh menggunakan pengecualian dalam destruktor Anda. Jadi, setidaknya dalam build debug, Anda dapat memverifikasi hasil pemanggilan fungsi dari dalam destruktor dengan WINRT_VERIFY (dengan ekspresi Boolean) dan WINRT_VERIFY_ (dengan hasil yang diharapkan serta ekspresi Boolean).

WINRT_VERIFY(::CloseHandle(value));
WINRT_VERIFY_(TRUE, ::CloseHandle(value));

API penting