异常边界:使用多个错误处理机制

David Blaikie

null

问候语!我很抱歉 大卫·布莱基 ,微软的软件设计工程师和博客嘉宾 虚拟博客 . 我在办公室工作 Windows产品组 编写C++中的测试基础设施工具。我工作的代码库的一个优点是它们相对现代和灵活。我们使用了完全的C++ 0x特性 Visual Studio 2010 包括 兰巴斯 R值引用/移动构造 可能的话。在更基本的层面上,我们也选择使用 例外情况 .

虽然我不会在这篇文章中讨论不同错误处理技术的优缺点,但是可以说我们使用了异常,但在某些情况下我们使用了依赖项(winapi,等等)并且我们的一些用户(我们对Windows测试代码源有API级别的公开)不公开异常API或者在启用异常的情况下构建他们的代码。为此,我们和许多其他开发人员一样,需要在一个可能不寻常的世界中过着不同寻常的生活。也许您遇到过类似的情况,并认为由于代码库的其余部分(或依赖项/使用者)没有使用异常,因此您自己的代码只能继承该设计选择。

事实上,您可以编写各种简单的技术、可重用的代码片段和小型库类,以便将异常代码与非异常的使用者和依赖项隔离开来。

非特殊依赖

处理不使用异常的依赖项(API函数、类)相当简单。我们先来举一个简单的普通代码的例子,看看它是如何转换的:

  1. BOOL DiffHandles(HANDLE file1,HANDLE file2);
  2. 布尔文件( 常数 世界卫生组织 *文件1, 常数 世界卫生组织 *文件2)
  3. {
  4. HANDLE file1Handle=CreateFile(file1,GENERICu READ,…);
  5. 布尔结果=假;
  6. 如果 (file1Handle!=无效的u句柄(值)
  7. {
  8. HANDLE file2Handle=CreateFile(file2,GENERICu READ,…);
  9. 如果 (file2Handle!=无效的u句柄(值)
  10. {
  11. 结果=DiffHandles(file1Handle,file2Handle);
  12. CloseHandle(file2Handle);
  13. }
  14. CloseHandle(file1Handle);
  15. }
  16. 返回 结果;
  17. }

第一件事:不要使用这个函数。它可以作为一个演示,但是作为真正的代码,它有很多错误(更不用说它是不完整的)。

虽然这段代码似乎没有太多的错误处理问题,但这只是因为错误消息处理是在一旁完成的。 创建文件 例如,指定 无效的u句柄u值 它将通过 错误信息 我们可以想象 扩散手柄 必须做同样的事情,从文件读取时看到类似的失败(例如,如果网络共享消失),并将这些都堆在一个错误的返回值下。的用户 DiffFiles文件 必须确保他们检查 错误信息 任何时候假象都会被归还给他们。他们不仅很容易忘记并且只是假设文件不同,而不是比较本身失败(可能是暂时的网络问题-文件比较失败,然后网络连接再次出现,文件恰好有匹配的内容)。这可能会导致问题),但此API的用户收到的故障也很模糊,显示给用户的任何消息都很难让用户理解或处理。虽然函数中的错误值可能表示某个文件[未找到,由于权限或任何其他原因而无法读取],但它可能不会说两个文件中的/哪个/有问题。

因此,我们的第一步可能是确保实际失败在测试(文件内容是否不同)返回false时产生异常。这将区分在尝试执行函数时发生的合法情况和运行时故障。在这种情况下,我们函数的类型保持不变(尽管我将切换到“bool”,因为我们没有直接与之交互的Win32旧版本),但约定是不同的:如果文件匹配,则返回true;如果文件不同,则返回false;如果无法确定文件是否匹配,则抛出异常。

为此,我们将引入一个简单的助手函数:

  1. 无效 抛掷器( 布尔 表情, 常数 世界卫生组织 *信息)
  2. {
  3. 如果 (!表达式)
  4. {
  5. Win32异常(GetLastError(),消息);
  6. }
  7. }

这不是这种函数的最高级形式。我们可以做更多的工作来创建更多的信息/人类可读的错误消息。也许 Win32异常 类型可以使用 格式化消息 从错误代码生成一条可读的消息,并以某种方式将我们的消息字符串附加到该消息上。在任何情况下,我们的函数现在可以重写如下:

  1. 布尔 DiffFiles文件( 常数 世界卫生组织 *文件1, 常数 世界卫生组织 *文件2)
  2. {
  3. HANDLE file1Handle=CreateFile(file1,GENERICu READ,?);
  4. ThrowLastErrorIf(file1Handle!=无效的u句柄u值,file1);
  5. HANDLE file2Handle=CreateFile(file2,GENERICu READ,…);
  6. ThrowLastErrorIf(file2Handle!=无效的u句柄u值,file2);
  7. BOOL result=DiffHandles(file1Handle,file2Handle);
  8. ThrowLastErrorIf(结果==FALSE&&(GetLastError()!=我的应用程序错误文件不匹配),L “无法比较文件内容” );
  9. CloseHandle(file1Handle);
  10. CloseHandle(file2Handle);
  11. 返回 结果;
  12. }

但是等等,我(希望我)听到你哭了,如果我们抛出异常,这个泄漏文件不会处理吗?你说得对。虽然我本可以编写这个修改版本来处理这个漏洞,但它会有些复杂,所以我将演示一个更好的方法。

通过将这些句柄打包在一个可以在多个C++中处理它们的类型中, 雷伊 通过这种方式,我们不仅可以使此代码更具可读性,而且还可以使其正确(无泄漏)

  1. 文件
  2. {
  3. 私有的 :
  4. 手柄;
  5. //声明但未定义以避免双重关闭
  6. 文件& 操作人员 =( 常数 文件(&);
  7. 文件(File&);
  8. 公众的 :
  9. 文件( 常数 世界卫生组织 *文件)
  10. {
  11. handle=CreateFile(文件,通用读取,…);
  12. ThrowLastErrorIf(句柄、文件);
  13. }
  14. 句柄Get()
  15. {
  16. 返回 手柄;
  17. }
  18. ~文件()
  19. {
  20. 关闭手柄(手柄);
  21. }
  22. };

再次重写原来的函数,我们得到:

  1. 布尔 DiffFiles文件( 常数 世界卫生组织 *文件1, 常数 世界卫生组织 *文件2)
  2. {
  3. 文件f1(文件1);
  4. 文件f2(文件2);
  5. 结果=DiffHandles(f1.Get(),f2.Get());
  6. ThrowLastErrorIf(结果==FALSE&&(GetLastError()!=我的应用程序错误文件不匹配),L “无法比较文件内容” );
  7. 返回 结果;
  8. }

所有这些都没有泄漏,需要密切关注代码路径,以确保资源破坏。

这和 扩散手柄 是一个异常感知API,但它稍微整理了一下函数,意味着异常依赖不会污染我们的异常感知代码库。

Win32Exception类型的实现和对 抛掷器 函数(包括将特定结果值映射到已知的异常类型,例如 标准::分配错误 )作为练习留给读者。

普通消费者

普通消费者类型

我们已经看到Win32“invalid return(FALSE,invalidu HANDLEu VALUE等)+GetLastError”是一种异常的错误消息方案。您可能遇到的其他具有非异常边界的API包括C代码(实际上,Win32的API是这种情况的一个特例,但是POSIX使用int返回值和errno来达到类似的效果)或HRESULT返回COM API。

虽然它可能不太明显,为什么它值得考虑C代码作为一个不寻常的消费者(“我的用户将在C中写入,所以我的库必须在C”我听到你哭)实际上是很可能写一个C API在C++中。通过声明函数 外部“C” 你可以在C++编译单元中使用C实现函数,在你的实现中使用C++编程语言的全部功能

与普通消费者打交道

与普通消费者打交道可能有点棘手,尽管最基本的实现不是非常困难,如果有点冗长和有损的话。让我们把上面的例子倒过来。想象一下我们有原始的 DiffFiles文件 ,但我们想保留接口(BOOL dou things)+ 错误信息 )但是我们已经更新了我们的依赖项(如图所示,使用RAII资源包装器进行文件处理,以及更新 扩散手柄 ,或使用STL)来识别异常。因此,他们不再返回失败通过 错误信息 ,而不是返回bool并为失败抛出异常。也许甚至没有 Win32异常 失败(这种特殊的异常类型不会被广泛使用,但只有在与非异常api交互时才可以使用,因为没有更准确的异常类型来表示失败)。  我们可以简单地重写 DiffFiles文件 功能如下:

  1. 布尔文件( 常数 世界卫生组织 *文件1, 常数 世界卫生组织 *文件2)
  2. {
  3. 尝试
  4. {
  5. 文件f1(文件1);
  6. 文件f2(文件2);
  7. 如果 (!DiffHandles(f1、f2))
  8. {
  9. SetLastError(我的应用程序错误文件不匹配);
  10. 返回 假;
  11. }
  12. 返回 是的;
  13. }
  14. 抓住 ( 常数 Win32E(异常(&e)
  15. {
  16. SetLastError(例如GetErrorCode());
  17. }
  18. 抓住 ( 常数 标准::例外(&e)
  19. {
  20. SetLastError(我的u应用程序u常规u错误);
  21. }
  22. 返回 假;
  23. }

您应该确保捕获try块中的代码可能产生的任何/所有异常。在本例中,我们知道File类和 扩散手柄 函数只能抛出 Win32异常 所以我们可以处理。

在这个基本实现中,我们丢失了所有异常细节,甚至那些我们可以映射到有趣结果的细节(也许 标准::分配错误 可能会映射到内存不足的错误代码(例如Win32错误代码),因此这并不理想。同样,我们可以想象放入各种catch块来将不同的异常类型映射到各种故障,添加日志记录来记录异常的全部细节(因为我们将压缩整个异常对象,包括上下文字符串、堆栈跟踪等,在合并为一个返回值等之前,我们的普通公共接口上的每一个函数都将变得冗长而笨拙。

宏作为异常使用边界

为了减少这种情况下的语法开销,我们可以使用宏实现一个方便的包装器来隐藏所有可能的复杂性和重复的逻辑:

  1. #定义 WIN32u启动 尝试 {
  2. #定义 WIN32u结束} 抓住 ( 常数 Win32Exception&e){SetLastError(e.GetErrorCode());} 抓住 ( 常数 std::exception&e){SetLastError(MY_APPLICATION_GENERAL_ERROR);} 返回 假;

dou things函数随后变为:

  1. 布尔文件( 常数 世界卫生组织 *文件1, 常数 世界卫生组织 *文件2)
  2. {
  3. WIN32u启动
  4. 文件f1(文件1);
  5. 文件f2(文件2);
  6. 如果 (!DiffHandles(f1、f2))
  7. {
  8. SetLastError(我的应用程序错误文件不匹配);
  9. 返回 假;
  10. }
  11. 返回 是的;
  12. WIN32u结束
  13. }

虽然宏提供了一种实现此功能的明显方法,但它们会使代码难以调试和分析。

lambda作为异常消耗边界

我们可以进一步整理一下,用lambda替换宏,如下所示:

  1. 模板 < 类别名 功能>
  2. BOOL Win32ExceptionBoundary(Func&&f)
  3. {
  4. 尝试
  5. {
  6. 返回 f();
  7. }
  8. 抓住 ( 常数 Win32E(异常(&e)
  9. {
  10. SetLastError(例如GetErrorCode());
  11. }
  12. 抓住 ( 常数 标准::例外(&e)
  13. {
  14. SetLastError(我的u应用程序u常规u错误);
  15. }
  16. 返回 假;
  17. }

使用此函数,我们现在可以将dou things()函数缩减为:

  1. 布尔文件( 常数 世界卫生组织 *文件1, 常数 世界卫生组织 *文件2)
  2. {
  3. 返回 Win32ExceptionBoundary([&]()->布尔
  4. {
  5. 文件f1(文件1);
  6. 文件f2(文件2);
  7. 如果 (!DiffHandles(f1、f2))
  8. {
  9. SetLastError(我的应用程序错误文件不匹配);
  10. 返回 假;
  11. }
  12. 返回 是的;
  13. });
  14. }

这个 Win32例外边界 可以泛化(所以它可以用于, 手柄 返回函数),例如,将错误结果作为一个额外参数,并使用该参数推断模板函数的返回类型。

摘要

使用这些工具,您可以将异常感知代码引入到代码库中,使您能够利用大量精心实现和测试的标准库,如容器、智能指针和算法,而无需修改整个代码库。当时间和业务合理性允许时,您的异常感知墙花园可以增长,一次转换单个函数/库。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享