Обработка ошибок в C++/WinRT

В этом разделе обсуждаются стратегии обработки ошибок при программировании на C++/WinRT. Общие и дополнительные сведения см. в статье Обработка ошибок и исключений (современный C++).

Избегайте создания и перехвата исключений

Мы рекомендуем по-прежнему писать код с обработкой исключений, но при этом избегать создания и перехвата исключений, когда это возможно. Если отсутствует обработчик исключения, Windows автоматически создает отчет об ошибках (в том числе минидамп сбоя), который поможет вам отследить проблему.

Не создавайте исключение, которое вы собираетесь перехватить. И не используйте исключения для ожидаемых ошибок. Создавайте исключение только при возникновении непредвиденной ошибки при исполнении, все остальные проблемы обрабатывайте с помощью кодов ошибок и результатов напрямую, рядом с источником сбоя. Таким образом, при вызове исключения вы будете знать, что его причина заключается в ошибке в вашем коде или в состоянии исключения системы.

Рассмотрим сценарий доступа к реестру Windows. Если вашему приложению не удается прочитать значение из реестра, следует ожидать ошибку, которую необходимо корректно обработать. Не вызывайте исключение, а верните значение bool или enum, указывающее, что значение не было прочитано и, возможно, почему это произошло. Тогда как ошибка записи значения в реестр скорее всего указывает на более серьезную проблему, которую невозможно обработать в вашем приложении. В таком случае не следует продолжать работу приложения, поэтому создание исключения, которое создает отчет об ошибках, — этой самый быстрый способ предотвращения дальнейших проблем.

В качестве другого примера рассмотрим ситуацию с получением эскиза изображения из вызова метода StorageFile.GetThumbnailAsync и последующей передачей этого эскиза в BitmapSource.SetSourceAsync. Если в результате такой последовательности вызовов nullptr передается в SetSourceAsync (не удалось прочитать файл изображения; возможно, файл не содержит изображение, хотя расширение указывает на обратное), это приведет к вызову исключения из-за недопустимого указателя. Если вы обнаружили подобное в своем коде, вместо перехвата и обработки исключения проверьте значение nullptr, возвращенное GetThumbnailAsync.

Создание исключений обычно занимает больше времени, чем использование кодов ошибок. Если вы вызываете исключение только при возникновении неустранимой ошибки, производительность программы будет оптимальной при ее нормальной работе.

Но ее производительность снизится из-за необходимости вызова соответствующих деструкторов при вызове исключения. Издержки такого решения зависят от того, вызывается исключение или нет. Поэтому вам необходимо убедиться, что компилятор дает хорошее представление о том, какие функции могут вызвать исключения. Если компилятор убедит вас в том, что определенные функции не будут вызывать исключений (спецификация noexcept), значит он может оптимизировать создаваемый код.

Перехват исключений

Ошибка, которая возникает на уровне ABI среды выполнения Windows, возвращается в виде значения HRESULT. Но вам не нужно обрабатывать HRESULT в коде. Код проекции C++/WinRT, который создается для API на стороне потребителя, обнаруживает код ошибки HRESULT на уровне ABI и преобразует код в исключение winrt::hresult_error, которое можно перехватить и обработать. Если вы точно хотите обработать значения HRESULT, тогда используйте тип winrt::hresult.

Например, если пользователь удалит изображение из библиотеки изображений, когда ваше приложение работает с этой коллекцией, проекция создаст исключение. И это тот случай, когда вам нужно перехватить и обработать такое исключение. Вот соответствующий пример кода.

#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.
        }
    }
}

Используйте этот шаблон в сопрограмме при вызове функции co_await. Еще одним примером такого преобразования HRESULT в исключение является ситуация, когда API компонента возвращает E_OUTOFMEMORY, что приводит к вызову исключения std::bad_alloc.

Если вы просто просматриваете код HRESULT, лучше использовать winrt::hresult_error::code. С другой стороны, функция winrt::hresult_error::to_abi выполняет преобразование в объект ошибки COM и передает состояние в локальную память потока COM.

Создание исключений

В некоторых ситуациях при сбое вызова определенной функции ваше приложение не сможет восстановить работу (вы больше не сможете рассчитывать на его предсказуемую работу). В примере кода ниже используется значение winrt::handle в качестве оболочки для значения HANDLE, возвращенного от CreateEvent. Затем дескриптор передается шаблону функции winrt::check_bool (с созданием значения bool). Функция winrt::check_bool работает с bool и любыми значениями, которые можно преобразовать в false (состояние ошибки) или true (состояние успеха).

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

Если значение, которое вы передаете функции winrt::check_bool, является false, выполняются следующие действия.

  • winrt::check_bool вызывает функцию winrt::throw_last_error.
  • winrt::throw_last_error вызывает GetLastError для извлечения значения последнего кода ошибки вызывающего потока, а затем вызывает функцию winrt::throw_hresult.
  • winrt::throw_hresult вызывает исключение, используя объект winrt::hresult_error (или стандартный объект), представляющий этот код ошибки.

Поскольку API Windows сообщают об ошибках во время выполнения с помощью различных типов возвращаемых значений, в дополнение к winrt::check_bool используется ряд других полезных вспомогательных функций для проверки значений и создания исключений.

  • winrt::check_hresult. Проверяет, представляет ли код HRESULT ошибку, и, если это так, вызывает winrt::throw_hresult.
  • winrt::check_nt. Проверяет, представляет ли код ошибку, и, если это так, вызывает winrt::throw_hresult.
  • winrt::check_pointer. Проверяет, имеет ли указатель значение NULL, и, если это так, вызывает winrt::throw_last_error.
  • winrt::check_win32. Проверяет, представляет ли код ошибку, и, если это так, вызывает winrt::throw_hresult.

Вы можете использовать эти вспомогательные функции для стандартных типов кода возврата или можете реагировать на любые ошибки и вызывать winrt::throw_last_error или winrt::throw_hresult.

Вызов исключений при создании API

Все границы двоичного интерфейса приложений (ABI) среды выполнения Windows (или ABI границы) должны быть помечены как noexcept. Это означает, что исключения не могут выходить за эти границы. При создании API следует всегда помечать границу ABI ключевым словом noexcept (C++). В C++ поведение noexcept специфично. Если исключение C++ достигает границы noexcept, процесс быстро завершается с помощью std::terminate. Это поведение является предпочтительным, так как необработанное исключение почти всегда подразумевает неизвестное состояние в процессе.

Так как исключение не должно выходить за границы ABI, на уровне ABI возвращается ошибка, которая возникает в реализации, в виде кода ошибки HRESULT. При создании API с использованием C++/WinRT автоматически создается код для преобразования всех исключений, которые вы вызываете в вашей реализации, в значение HRESULT. Функция winrt::to_hresult используется в этом коде следующим образом.

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 обрабатывает исключения, производные от std::exception, winrt::hresult_error и ее производных типов. В реализации лучше всего использовать winrt::hresult_error или производный тип, чтобы пользователи вашего API получали расширенные сведения об ошибке. Значение std::exception (которое сопоставляется с E_FAIL) поддерживается, если исключения возникают при использовании стандартной библиотеки шаблонов.

Возможность отладки при использовании noexcept

Как упоминалось выше, если в C++ исключение достигает границы noexcept, процесс быстро завершается с помощью std::terminate. Это неидеальный вариант для отладки, так как std::terminate часто частично или полностью не учитывает контекст возникающей ошибки или исключения, в особенности при использовании сопрограмм.

Поэтому в этом разделе рассматривается ситуация, когда метод ABI (который вы правильно пометили noexcept) использует co_await для вызова асинхронного кода проекции C++/WinRT. Рекомендуется заключить вызовы в код проекции C++/WinRT в winrt::fire_and_forget. Это позволяет правильно записать в подходящем месте необработанное исключение в виде исключения заполнения, что существенно увеличивает шансы на выполнение отладки.

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 имеет встроенный вспомогательный метод unhandled_exception, который вызывает winrt::terminate. Последний, в свою очередь, вызывает RoFailFastWithErrorContext. Это гарантирует, что любой контекст (исключение заполнения, код ошибки, сообщение об ошибке, обратная трассировка стека и т. д.) сохраняется для отладки в режиме реального времени или для дампа после неустранимой ошибки. Для удобства можно выделить часть "выполнить и забыть" в отдельную функцию, возвращающую winrt::fire_and_forget, а затем выполняющую вызов.

Синхронный код

В некоторых случаях метод ABI (который, опять же, правильно помечен noexcept) вызывает только синхронный код. Иными словами, он никогда не использует co_await, чтобы вызывать асинхронный метод среды выполнения Windows или переключаться между активными и фоновыми потоками. В этом случае методика "выполнить и забыть" будет работать, но это будет неэффективно. Вместо этого можно сделать примерно следующее.

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

Быстрое завершение со сбоем

Выполнение кода из предыдущего раздела все так же быстро завершается сбоем. Как уже упоминалось, этот код не обрабатывает исключения. Любое необработанное исключение приводит к завершению программы.

Но такая форма предпочтительна, так как она обеспечивает возможность отладки. В редких случаях может потребоваться выполнить try/catch и обработать определенные исключения. Но это должны быть редкие случаи, так как мы не рекомендуем использовать исключения в качестве механизма управления потоками для обеспечения требуемых условий (чему и посвящена эта статья).

Помните о том, что не следует позволять необработанному исключению не учитывать контекст noexcept. В таком случае среда выполнения C++ прекращает процесс с помощьюstd::terminate, теряя при этом все сведения об исключении заполнения, которые собирает C++/WinRT.

Проверочные утверждения

Для проверки внутренних предположений в приложении используются проверочные утверждения. Когда это возможно, используйте static_assert для проверки во время компиляции. Во время выполнения используйте WINRT_ASSERT с логическим выражением. WINRT_ASSERT — это макроопределение, которое передается в _ASSERTE.

WINRT_ASSERT(pos < size());

В сборках выпуска WINRT_ASSERT компилируется, а в отладочной сборке это утверждение останавливает приложение в отладчике на строке кода, где находится проверочное утверждение.

Не следует использовать исключения в деструкторах. Таким образом, по крайней мере в отладочных сборках вы можете использовать проверочное утверждение результата вызова функции из деструктора с помощью WINRT_VERIFY (с логическим выражением) и WINRT_VERIFY_ (с ожидаемым результатом и логическим выражением).

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

Важные API