Penanganan kesalahan dengan C++/WinRT

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

Hindari menangkap dan melemparkan pengecualian

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

Jangan memberikan pengecualian yang Anda harapkan untuk ditangkap. Dan jangan gunakan pengecualian untuk kegagalan yang diharapkan. Lemparkan pengecualian hanya ketika terjadi kesalahan runtime yang tidak terduga, dan tangani yang lainnya 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 keadaan kesalahan yang luar biasa dalam sistem.

Pertimbangkan skenario mengakses Windows Registry. Jika aplikasi Anda gagal membaca nilai dari Registry, maka itu yang diharapkan, dan Anda harus menanganinya dengan anggun. Jangan memberikan pengecualian; lebih tepatnya mengembalikan nilai atau enum yang bool menunjukkan bahwa, dan mungkin mengapa, nilainya tidak dibaca. Gagal menulis nilai ke Registry, di sisi lain, kemungkinan akan menunjukkan bahwa ada masalah yang lebih besar daripada yang dapat Anda tangani dengan bijaksana 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 agar tidak menyebabkan kerusakan.

Untuk contoh lain, pertimbangkan untuk mengambil gambar thumbnail dari panggilan ke StorageFile.GetThumbnailAsync, lalu meneruskan thumbnail itu ke BitmapSource.SetSourceAsync. Jika urutan panggilan itu 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 penunjuk 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.

Melempar pengecualian cenderung lebih lambat daripada menggunakan kode kesalahan. Jika Anda hanya memberikan pengecualian ketika kesalahan fatal terjadi, maka jika semuanya berjalan dengan baik, Anda tidak akan pernah membayar harga kinerja.

Tetapi hit kinerja yang lebih mungkin melibatkan overhead runtime untuk memastikan bahwa destructors yang sesuai dipanggil jika pengecualian dilemparkan. Biaya jaminan ini datang apakah pengecualian benar-benar dilemparkan atau tidak. Jadi, Anda harus memastikan bahwa kompiler memiliki ide bagus tentang fungsi apa yang berpotensi memberikan pengecualian. Jika kompiler dapat membuktikan bahwa tidak akan ada pengecualian dari fungsi tertentu ( noexcept spesifikasi), maka 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 HRESULTs dalam kode Anda. Kode proyeksi C ++/WinRT yang dihasilkan untuk API di sisi konsumsi mendeteksi kesalahan kode HRESULT pada lapisan ABI dan mengubah 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 Perpustakaan Gambar saat aplikasi Anda mengulangi koleksi itu, maka proyeksi tersebut memberikan 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 ketika API komponen mengembalikan E_OUTOFMEMORY, yang menyebabkan std::bad_alloc dilemparkan.

Lebih suka winrt::hresult_error::code saat Anda baru saja mengintip kode HRESULT. Fungsi winrt::hresult_error::to_abi di sisi lain berubah menjadi objek kesalahan COM, dan mendorong status ke penyimpanan lokal utas COM.

Melempar 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 diprediksi). Contoh kode di bawah ini menggunakan nilai winrt::handle sebagai pembungkus di sekitar HANDLE yang dikembalikan dari CreateEvent. Kemudian melewati pegangan (membuat bool nilai darinya) ke template 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 lewati 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 penunjuk 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 menanggapi kondisi kesalahan apa pun dan memanggil winrt::throw_last_error atau winrt::throw_hresult.

Melempar pengecualian saat menulis API

Semua Windows batas Antarmuka Biner Aplikasi Runtime (atau batas ABI) tidak boleh ada—artinya pengecualian tidak boleh luput dari sana. Saat Anda menulis API, Anda harus selalu menandai batas ABI dengan kata kunci C ++ noexcept . noexcept memiliki perilaku khusus dalam C ++. Jika pengecualian C ++ mencapai noexcept batas, maka proses akan gagal dengan cepat dengan std::terminate. Perilaku itu umumnya diinginkan, karena pengecualian yang tidak ditangani hampir selalu menyiratkan keadaan 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 mengonversi pengecualian apa pun yang Anda lemparkan ke dalam implementasi Anda menjadi 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 Perpustakaan Template Standar oleh Anda.

Debuggability dengan noexcept

Seperti yang kami sebutkan di atas, pengecualian C ++ yang noexcept mencapai 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 beri anotasi dengan noexceptbenar) digunakan co_await untuk memanggil kode proyeksi C ++/ WinRT asinkron. Sebaiknya bungkus panggilan ke kode proyeksi C++/WinRT dalam winrt::fire_and_forget. Melakukan hal itu memberikan tempat yang tepat untuk pengecualian yang tidak ditangani untuk dicatat dengan benar sebagai pengecualian yang disimpan, 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 yang disimpan, kode kesalahan, pesan kesalahan, tumpukan backtrace, dan sebagainya) dipertahankan baik untuk debugging langsung atau untuk dump post-mortem. Untuk kenyamanan, Anda dapat memasukkan bagian api-dan-lupa ke dalam fungsi terpisah yang mengembalikan winrt: : fire_and_forget, dan kemudian menyebutnya.

Kode sinkron

Dalam beberapa kasus, metode ABI Anda (yang, sekali lagi, Anda telah dianotasi dengan noexceptbenar) hanya memanggil kode sinkron. Dengan kata lain, ia tidak pernah menggunakan co_await, baik untuk memanggil metode runtime Windows asinkron, atau untuk beralih antara utas latar depan dan latar belakang. Dalam hal ini, teknik fire_and_forget masih akan berhasil, tetapi tidak efisien. Sebaliknya, 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 tertulis, kode itu tidak menangani pengecualian apa pun. Setiap pengecualian yang tidak ditangani menghasilkan penghentian program.

Tetapi bentuk itu lebih unggul, karena memastikan debuggabilitas. Dalam kasus yang jarang terjadi, Anda mungkin ingin try/catch, dan menangani pengecualian tertentu. Tapi itu seharusnya 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 ditangani melarikan diri dari konteks telanjang noexcept . Dalam kondisi itu, runtime C ++ akan menghentikan proses, sehingga kehilangan informasi pengecualian yang disimpan yang dicatat dengan cermat oleh C ++/WinRT.

Pernyataan

Untuk asumsi internal dalam aplikasi Anda, ada pernyataan. Lebih suka 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 dalam build rilis; dalam build debug, itu menghentikan aplikasi di debugger pada baris kode di mana pernyataan itu berada.

Anda tidak boleh menggunakan pengecualian dalam destructors Anda. Jadi, setidaknya dalam build debug, Anda dapat menegaskan hasil memanggil fungsi dari destructor 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