本主题讨论使用 C++/WinRT 编程时处理错误的策略。 有关常规信息和背景信息,请参阅错误和异常处理(新式 C++)。
避免捕获和抛出异常
我们建议你继续编写 异常安全代码,但你希望尽可能避免捕获和引发异常。 如果没有异常处理程序,Windows 会自动生成错误报告(包括崩溃的小型转储文件),这将帮助你查明问题出在哪里。
不要抛出你预期会捕获的异常。 不要用异常来处理可预期的失败情况。 仅在发生意外的运行时错误时才抛出异常,其他所有情况都应直接在靠近故障源的位置使用错误码/结果码进行处理。 这样一来, 当引发异常 时,你知道原因是代码中的 bug 或系统中的异常错误状态。
请考虑访问Windows注册表的方案。 如果应用无法从注册表中读取某个值,这是意料之中的情况,你应该妥善处理。 不要抛出异常;而是返回一个 bool 或 enum 值,以指示该值未被读取,以及可能未被读取的原因。 另一方面,未能将某个值写入注册表,很可能表明存在一个比你的应用程序所能妥善处理的更严重的问题。 在这种情况下,你不希望应用程序继续,因此导致错误报告的异常是阻止应用程序造成任何伤害的最快方法。
对于另一个示例,请考虑从调用 StorageFile.GetThumbnailAsync 检索缩略图图像,然后将该缩略图传递给 BitmapSource.SetSourceAsync。 如果该调用序列导致你将 nullptr 传递给 SetSourceAsync(图像文件无法读取;也许它的文件扩展名让它看起来像是包含图像数据,但实际上并不包含),那么就会引发无效指针异常。 如果你在代码中发现这种情况,不要将其作为异常来捕获和处理,而应检查 GetThumbnailAsync 是否返回了 nullptr。
抛出异常通常比使用错误码更慢。 如果只有在发生致命错误时才抛出异常,那么在一切正常的情况下,你就永远不需要付出性能代价。
但更可能的性能命中涉及运行时开销,以确保在引发异常的不太可能的情况下调用适当的析构函数。 这种保障的代价是,无论异常是否真的被抛出,都必须付出。 因此,应确保编译器很好地了解哪些函数可能会引发异常。 如果编译器可以证明某些函数 noexcept (规范)中没有任何异常,则它可以优化它生成的代码。
捕获异常
Windows 运行时 ABI 层中出现的错误条件以 HRESULT 值的形式返回。 但无需在代码中处理 HRESULT。 为使用端的 API 生成的 C++/WinRT 投影代码会检测 ABI 层的错误 HRESULT 代码,并将代码转换为 winrt::hresult_error 异常,你可以捕获和处理该异常。 如果你确实希望处理 HRESULT,那么请使用 winrt::hresult 类型。
例如,如果用户在应用程序循环访问该集合时从图片库中删除图像,则投影将引发异常。 在这种情况下,必须捕获和处理该异常。 下面是显示此情况的代码示例。
#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.
}
}
}
在协程中调用由 co_await 修饰的函数时,使用相同的模式。 这种 HRESULT 到异常的转换的另一个示例是,当组件 API 返回 E_OUTOFMEMORY 时,就会引发 std::bad_alloc 异常。
如果你只是查看一下 HRESULT 代码,优先使用 winrt::hresult_error::code。 另一方面,winrt::hresult_error::to_abi 函数会将其转换成 COM 错误对象,并将状态压入 COM 线程本地存储。
引发异常
在某些情况下,如果对给定函数的调用失败,应用程序将无法恢复(你将无法再依赖它以可预测的方式运行)。 下面的代码示例使用 winrt::handle 值来包装从 CreateEvent 返回的 HANDLE。 然后,它将句柄(从它创建 bool 值)传递给 winrt::check_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 对象(或标准对象)引发异常。
由于 Windows API 使用各种返回值类型来报告运行时错误,因此,除了 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 时抛出异常
所有 Windows 运行时 应用程序二进制接口 边界(或 ABI 边界)都必须为 noexcept——这意味着异常绝不能从这些边界逸出。 创作 API 时,应始终使用 C++ noexcept 关键字标记 ABI 边界。
noexcept 在 C++ 中具有特定行为。 如果 C++ 异常命中 noexcept 边界,则进程将快速失败并出现 std::terminate。 这种行为通常是可取的,因为未经处理的异常几乎总是意味着进程中的未知状态。
由于异常不得跨越 ABI 边界,因此实现中出现的错误条件以 HRESULT 错误代码的形式在 ABI 层中返回。 使用 C++/WinRT 创作 API 时,会生成代码,以便将你在实现 中引发的任何 异常转换为 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。 这可确保保留所有上下文信息(如暂存异常、错误代码、错误消息、堆栈回溯等),以供实时调试或事后分析转储使用。 为方便起见,可以将 fire-and-forget 部分分解为返回 winrt::fire_and_forget 的单独函数,然后调用该部分。
同步代码
在某些情况下,您的 ABI 方法(再次强调,您已用 noexcept 对其进行了正确标注)只调用同步代码。 换句话说,它从不使用 co_await,无论是为了调用异步的 Windows 运行时 方法,还是为了在前台线程与后台线程之间切换。 在这种情况下,fire_and_forget 方法仍然可行,但效率较低。 不过,你可以这样做。
HRESULT abi() noexcept try
{
// ABI code goes here.
} catch (...) { winrt::terminate(); }
快速失败
上一部分中的代码仍然快速失败。 正如编写的那样,该代码不会处理任何异常。 任何未经处理的异常都会导致程序终止。
但这种形式是优越的,因为它可确保可调试性。 在极少数情况下,你可能想要 try/catch,并处理某些异常情况。 但是,这应该很少见,因为正如本主题所解释的那样,我们不建议将异常用作预期条件的流控制机制。
请记住,让未经处理的异常从裸 noexcept 上下文中逸出是个坏主意。 在这种情况下,C++ 运行时将调用 std::terminate 终止该进程,从而丢失 C++/WinRT 精心记录的任何暂存异常信息。
Assertions
对于应用程序中的内部假设,可以使用断言。 尽可能首选 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
- winrt::check_bool 函数模板
- winrt::check_hresult 函数
- winrt::check_nt 函数模板
- winrt::check_pointer 函数模板
- winrt::check_win32 函数模板
- winrt::handle 结构体
- winrt::hresult_error 结构
- winrt::throw_hresult 函数
- winrt::throw_last_error 函数
- winrt::to_hresult 函数