Error handling with C++/WinRT
Error handling with C++/WinRT
异常最好只用来处理异常。而不是用来处理可预期错误。用throw/catch的方式处理可预期错误,不仅影响性能,而且影响代码可读性。异常用于将那些不可预期,无法在本地处理的错误沿调用栈回溯,知道遇到一个能够处理此异常的调用者为止。实在没有处理者,让程序终止。
举了例子,假设要用StorageFile.GetThumbnailAsync获取缩略图,并传给BitmapSource.SetSourceAsync。前者可能会返回nullptr,这是一个可预期的错误,程序员应该自己想办法处理好这个nullptr,而不是抛出异常。
WinRT ABI是通过返回HRESULT(32位整型)的方式返回错误代码的。在WinRT映射类中,异常跨越边界的时候被转化为HResult,然后被转为winrt::hresult_error 异常。你也可以使用winrt::hresult处理ABI传入的HRESULT。
如果你要抛出异常,C++/WinRT也提供了一些支持,下面是个例子:
winrt::handle h{ ::CreateEvent(nullptr, false, false, nullptr) };
winrt::check_bool(bool{ h });
winrt::check_bool(::SetEvent(h.get()));
如果winrt::check_bool失败,那么会调用winrt::throw_last_error,然后调用GetLastError 获取当前线程的错误值,然后调用winrt::throw_hresult 将错误转化成winrt::hresult_error抛出。
其他check函数:
- winrt::check_hresult. Checks whether the HRESULT code represents an error and, if so, calls winrt::throw_hresult.
- winrt::check_nt. Checks whether a code represents an error and, if so, calls winrt::throw_hresult.
- winrt::check_pointer. Checks whether a pointer is null and, if so, calls winrt::throw_last_error.
- winrt::check_win32. Checks whether a code represents an error and, if so, calls winrt::throw_hresult.
尽量多写异常安全地代码,避免捕获和抛出异常。如果程序抛出地异常没有处理,那么Windows自动生成一个错误报告(包含一个minidump),帮你分析这个异常。
所谓异常,应该都是预料之外地,如果你用catch去捕获一个异常地时候,想想这么做地合理性。这样当异常产生地时候,一定是你地代码逻辑有问题,或者系统运行出错。
也就是说异常最好只用来处理异常。而不是用来处理可预期错误。用throw/catch的方式处理可预期错误,不仅影响性能,而且影响代码可读性。异常用于将那些不可预期,无法在本地处理的错误沿调用栈回溯,知道遇到一个能够处理此异常的调用者为止。实在没有处理者,让程序终止。
抛出异常可比返回错误码要更影响性能
举了例子,假设要用StorageFile.GetThumbnailAsync获取缩略图,并传给BitmapSource.SetSourceAsync。前者可能会返回nullptr,这是一个可预期的错误,程序员应该自己想办法处理好这个nullptr,而不是抛出异常。
这些抛异常地助手都是通过winrt::throw_last_error or winrt::throw_hresult.来最终抛出异常地
ABI界面必须是noexcept的。在noexcept的函数中抛出异常,会导致std::terminate被调用,从而中止程序。C++/WinRT做成的API会自动保障ABI边界的noexcept要求,然后再投射类中通过winrt::to_hresult将HRESULT转化为异常。
winrt::to_hresult可以处理std::exception,以及从winrt::hresult_error派生出来的异常。
前面提到异常碰到noexcept会调用std::terminate()。这里的一个问题是std::terminate()不会保留异常的抛出痕迹,特别是在coroutine中使用的时候。建议在调用投射方法的时候,将代码包裹在winrt::fire_and_forget,以便增加调试性。(winrt::fire_and_forget调用winrt::terminate,间接调用RoFailFastWithErrorContext来保存更多上下文信息)
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;
}
对于同步的代码,可以使用外围try/catch:
HRESULT abi() noexcept try
{
// ABI code goes here.
} catch (...) { winrt::terminate(); }
对于调试环境下的代码,可以通过WINRT_ASSERT 来进行断言,这个宏会转化为_ASSERTE,在发布环境下,这个断言会消失。
在析构函数下,必须保证资源均被正确释放,推荐使用WINRT_VERIFY_:
WINRT_VERIFY(::CloseHandle(value));
WINRT_VERIFY_(TRUE, ::CloseHandle(value));
Modern C++ best practices for exceptions and error handling
在传统COM编程中,错误会通过HRESULT返回,或者函数特殊的传出参数。在Win32程序中,可以通过GetLastError 来获取上一次的错误。这些都是通过返回值来反映错误状态。
现代C++程序倾向于使用异常来处理逻辑和运行错误。逻辑错误指的是编写代码时考虑不周导致的错误,而运行错误是程序所依赖的运行支持系统产生的错误。使用异常的好处如下:
- 通过返回值返回的错误可以忽略,但是异常不可忽略。
- 异常可以沿着栈回溯到一个异常处理点,错误发生点和处理点可以分离
- 异常发生时栈反解的过程可以保证资源得到释放
健壮的错误处理是编程时候的一大挑战,如果合理使用异常?下面是一些指导原则
- 使用断言来检查不该发生的错误,使用异常来监测有可能发生的错误
- 异常是为了在离错误发生地之外处理错误,如果做不到的话,使用返回值可能是更好的办法
- 异常会对性能有所 影响,性能关键的部分应该避免使用异常
- 对于会抛出异常的函数,提供三个等级的保证:强保证,弱保证,无异常保证
- 抛出异常的时候应当传值,捕获异常的时候应该使用引用,不要捕获无法处理的异常
- 不要使用异常声明(在C++11中废弃)
- 使用标准库提供的异常体系
- 不应该允许异常从析构或者内存释放函数中抛出
Exceptions and performance
不发生异常的时候,对性能影响不大。发生异常的时候,回溯栈的代价跟函数调用差不多。当然,在try语句块中,编译器需要生成额外的代码来设置异常处理,帮助栈反解。在内存受限的系统中,异常的使用比较需要比较谨慎。在性能攸关的循环中,要避免异常的额外开销。总的来说,要通过性能剖析来判断异常的影响。
Exceptions vs. assertions
异常和断言是两种机制。断言是用来验证假设,当假设不成立,程序应当停止运行。断言失败时会立即中止程序。而异常发生时,程序有可能继续运行。
C++ exceptions versus Windows SEH exceptions
Windows提供了structured exception handling (SEH)的机制,在C/C++中都可以使用。SEH 提供了__try, __except, and __finally等关键字。总的来说,应该使用C++自带的异常,而不是SEH。
Exception specifications and noexcept
C++ 11废弃了异常声明(throw(...)
),不应该再使用它。
How to: Design for exception safety
健壮的错误处理要求再一开始就进行规划。程序调用了底层的代码,有可能产生异常。但是底层代码不知道上层应用的意图,所以无法提供有针对性地的异常处理。底层代码只有在能够自动恢复地情况下才应该吞下异常,否则就交给上层决定。
下面是一些技巧,帮助设计异常安全地代码(也就是异常发生时能够正确处理好资源问题)。
Keep resource classes simple
把资源类设计得简单一些,以避免资源泄露。尽可能使用智能指针。
Use the RAII idiom to manage resources
Resource Acquisition Is Initialization (RAII) 通过栈上对象得生命周期来管理资源得生命周期,以保证资源得自动释放。
像unique_ptr和shared_ptr 函数,其资源是用户分配得,所以不能完全算RAII,而是算Resource Release Is Destruction。
The three exception guarantees
异常的三级保证。
No-fail guarantee
保证无异常。最高的异常安全等级。经过仔细编写,保证一个函数不抛出异常。析构函数通常是需要保证无异常的。所有标准库中的容器要求传入的对象在析构的时候不发生异常。
Strong guarantee
保证稳定态。当函数在异常发生的时候,保证内存能够得到正确释放,并且程序状态能够维持正常。
Basic guarantee
保证无泄漏。当函数在异常发生的时候,保证内存能够得到正确释放,不要求程序状态能够维持正常。
Exception-safe classes
内建类型都是保证无异常的。标准库支持保证无泄漏。对于自定义类型,应该注意:
- 使用智能指针或者其他RAII来管理资源。如果一个类是专用于管理一种资源的,允许使用其析构函数来管理资源。
- 基类构造函数抛出的异常无法在派生类析构的内部捕获,需要使用函数级别的try/catch才行
- 在概念上,C++不允许非初始化或者部分初始化的类型。这意味着,一个构造函数,要么成功要么失败。
- 一个基本的公里,不要让异常逃出析构函数,并在栈上回溯。析构函数必须在内部消化掉异常,否则就应该中止程序。
其他
- Exceptions and Stack Unwinding in C++
- How to: Interface between exceptional and non-exceptional code
- Errors and Exception Handling (Modern C++)
- https://docs.microsoft.com/en-us/windows/uwp/cpp-and-winrt-apis/use-csharp-component-from-cpp-winrt
- https://docs.microsoft.com/en-us/windows/uwp/cpp-and-winrt-apis/natvis
(本篇完)