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 melempar pengecualian

Kami menyarankan agar Anda terus menulis kode pengecualian-aman, tetapi Anda lebih suka menghindari penangkapan dan melemparkan pengecualian jika memungkinkan. Jika tidak ada handler untuk pengecualian, maka Windows secara otomatis menghasilkan laporan kesalahan (termasuk minidump crash), yang akan membantu Anda melacak di mana masalahnya berada.

Jangan melemparkan pengecualian yang Anda harapkan untuk menangkap. Dan jangan gunakan pengecualian untuk kegagalan yang diharapkan. Lempar pengecualian hanya ketika kesalahan runtime yang tidak terduga terjadi, dan tangani yang lain dengan kode kesalahan/hasil—secara langsung, dan dekat 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 Windows Registry. Jika aplikasi Anda gagal membaca nilai dari Registri, maka itu yang diharapkan, dan Anda harus menanganinya dengan baik. Jangan melemparkan pengecualian; sebaliknya mengembalikan bool nilai atau enum yang menunjukkan bahwa, dan mungkin mengapa, nilainya tidak dibaca. Gagal menulis nilai ke Registri, di sisi lain, kemungkinan akan menunjukkan bahwa ada masalah yang lebih besar daripada yang dapat Anda tangani dengan sensif di 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, daripada menangkap dan menangani kasus sebagai pengecualian, sebagai gantinya periksa untuk nullptr dikembalikan dari GetThumbnailAsync.

Melemparkan pengecualian cenderung lebih lambat daripada menggunakan kode kesalahan. Jika Anda hanya melemparkan pengecualian ketika kesalahan fatal terjadi, maka jika semua berjalan dengan baik, Anda tidak akan pernah membayar harga performa.

Tetapi hit performa yang lebih mungkin melibatkan overhead runtime untuk memastikan bahwa destruktor yang sesuai dipanggil dalam peristiwa yang tidak mungkin bahwa pengecualian dilemparkan. Biaya jaminan ini muncul apakah pengecualian benar-benar dilemparkan atau 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.

Menangkap pengecualian

Kondisi kesalahan yang muncul pada lapisan Windows Runtime ABI 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 menggunakan 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 HRESULTS, 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/Windows.UI.Xaml.Media.Imaging.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Storage;
using namespace Windows::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 ini dalam coroutine saat memanggil co_awaitfungsi -ed. Contoh lain dari konversi HRESULT-to-exception ini adalah bahwa ketika API komponen mengembalikan E_OUTOFMEMORY, yang menyebabkan std::bad_alloc dilemparkan.

Lebih suka winrt::hresult_error::code saat Anda hanya mengintip kode HRESULT. Fungsi winrt::hresult_error::to_abi di sisi lain mengonversi ke objek kesalahan COM, dan mendorong status ke penyimpanan com thread-local.

Melemparkan 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 handel (membuat bool nilai 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 melempar 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—yang berarti bahwa pengecualian tidak boleh lolos di sana. 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 noexcept batas, maka proses akan gagal dengan cepat 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 memetakan ke E_FAIL) didukung jika pengecualian muncul dari penggunaan Pustaka Templat Standar.

Debuggability dengan noexcept

Seperti yang kami sebutkan di atas, pengecualian C++ yang mencapai noexcept batas gagal dengan cepat 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 dalam winrt::fire_and_forget. Melakukannya menyediakan tempat yang tepat untuk pengecualian yang tidak tertangani untuk direkam dengan benar sebagai pengecualian tersimpan, yang sangat meningkatkan debuggability.

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. Ini menjamin bahwa konteks apa pun (pengecualian tersimpan, kode kesalahan, pesan kesalahan, backtrace tumpukan, dan sebagainya) dipertahankan baik untuk penelusuran kesalahan langsung atau untuk cadangan pasca-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, ini 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 cepat

Kode di bagian sebelumnya masih gagal dengan cepat. 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 yang jarang terjadi, 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 itu, runtime C++ akan std::mengakhiri proses, sehingga kehilangan informasi pengecualian tersimpan yang direkam dengan hati-hati C++/WinRT.

Pernyataan

Untuk asumsi internal dalam aplikasi Anda, ada pernyataan. 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 dikompilasi jauh dalam build rilis; dalam build debug, ini menghentikan aplikasi di debugger pada baris kode tempat pernyataan berada.

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

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

API penting