COM (Win32 和 C++) 入门中的错误处理
COM 使用 HRESULT 值来指示方法或函数调用的成功或失败。 各种 SDK 标头定义各种 HRESULT 常量。 WinError.h 中定义了一组常见的系统范围代码。 下表显示了其中一些系统范围的返回代码。
常数 | 数值 | 说明 |
---|---|---|
E_ACCESSDENIED | 0x80070005 | 访问被拒绝。 |
E_FAIL | 0x80004005 | 错误。 |
E_INVALIDARG | 0x80070057 | 参数值无效。 |
E_OUTOFMEMORY | 0x8007000E | 内存不足。 |
E_POINTER | 0x80004003 | 错误地为指针值传递 NULL。 |
E_UNEXPECTED | 0x8000FFFF | 意外情况。 |
S_OK | 0x0 | 成功。 |
S_FALSE | 0x1 | 成功。 |
前缀为“E_”的所有常量都是错误代码。 S_OK和S_FALSE常量都是成功代码。 可能 99% 的 COM 方法在成功时返回 S_OK ;但不要让这个事实误导你。 方法可能会返回其他成功代码,因此始终使用 SUCCEEDED 或 FAILED 宏测试错误。 以下示例代码演示了测试函数调用是否成功的错误方式和正确方法。
// Wrong.
HRESULT hr = SomeFunction();
if (hr != S_OK)
{
printf("Error!\n"); // Bad. hr might be another success code.
}
// Right.
HRESULT hr = SomeFunction();
if (FAILED(hr))
{
printf("Error!\n");
}
S_FALSE的成功代码值得提及。 某些方法使用 S_FALSE 来大致表示非失败的负条件。 它还可能指示“no-op”-该方法成功,但没有效果。 例如,如果从同一线程再次调用 CoInitializeEx 函数,则返回 S_FALSE 。 如果需要区分代码中的 S_OK 和 S_FALSE ,则应直接测试值,但仍使用 FAILED 或 SUCCEEDED 来处理剩余情况,如以下示例代码所示。
if (hr == S_FALSE)
{
// Handle special case.
}
else if (SUCCEEDED(hr))
{
// Handle general success case.
}
else
{
// Handle errors.
printf("Error!\n");
}
某些 HRESULT 值特定于 Windows 的特定功能或子系统。 例如,Direct2D 图形 API 定义D2DERR_UNSUPPORTED_PIXEL_FORMAT错误代码,这意味着程序使用的像素格式不受支持。 MSDN 文档通常提供方法可能返回的特定错误代码列表。 但是,不应认为这些列表是明确的。 方法始终可以返回文档中未列出的 HRESULT 值。 同样,请使用 SUCCEEDED 和 FAILED 宏。 如果测试特定错误代码,则还包括默认情况。
if (hr == D2DERR_UNSUPPORTED_PIXEL_FORMAT)
{
// Handle the specific case of an unsupported pixel format.
}
else if (FAILED(hr))
{
// Handle other errors.
}
错误处理模式
本部分介绍一些以结构化方式处理 COM 错误的模式。 每种模式各有优缺点。 在某种程度上,选择是一个口味的问题。 如果处理现有项目,它可能已有禁止特定样式的编码准则。 无论采用哪种模式,可靠的代码都将遵守以下规则。
- 对于返回 HRESULT 的每个方法或函数,检查返回值,然后再继续。
- 使用资源后释放资源。
- 请勿尝试访问无效或未初始化的资源,例如 NULL 指针。
- 释放资源后,请勿尝试使用资源。
考虑到这些规则,下面是用于处理错误的四种模式。
嵌套 ifs
每次调用返回 HRESULT 后,请使用 if 语句来测试是否成功。 然后,将下一个方法调用置于 if 语句的范围内。 更多 if 语句可以根据需要嵌套得更深。 本模块中前面的代码示例都使用了此模式,但此处再次显示:
HRESULT ShowDialog()
{
IFileOpenDialog *pFileOpen;
HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
if (SUCCEEDED(hr))
{
hr = pFileOpen->Show(NULL);
if (SUCCEEDED(hr))
{
IShellItem *pItem;
hr = pFileOpen->GetResult(&pItem);
if (SUCCEEDED(hr))
{
// Use pItem (not shown).
pItem->Release();
}
}
pFileOpen->Release();
}
return hr;
}
优点
- 可以使用最小范围声明变量。 例如,在使用 pItem 之前不会声明 pItem 。
- 在每个 if 语句中,某些固定项为 true:以前的所有调用都成功,并且所有获取的资源仍然有效。 在前面的示例中,当程序到达最内部的 if 语句时, 已知 pItem 和 pFileOpen 都是有效的。
- 明确何时释放接口指针和其他资源。 在 if 语句的末尾释放一个资源,该语句紧跟在获取资源的调用之后。
缺点
- 有些人发现深嵌套很难阅读。
- 错误处理与其他分支和循环语句混合在 中。 这会使整个程序逻辑更难遵循。
级联 ifs
每次调用方法后,使用 if 语句测试是否成功。 如果方法成功,请将下一个方法调用置于 if 块内。 但是,不要进一步嵌套 if 语句,而是将每个后续 的 SUCCEEDED 测试置于上一 个 if 块之后。 如果任何方法失败,则所有剩余的 SUCCEEDED 测试只会失败,直到达到函数的底部。
HRESULT ShowDialog()
{
IFileOpenDialog *pFileOpen = NULL;
IShellItem *pItem = NULL;
HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
if (SUCCEEDED(hr))
{
hr = pFileOpen->Show(NULL);
}
if (SUCCEEDED(hr))
{
hr = pFileOpen->GetResult(&pItem);
}
if (SUCCEEDED(hr))
{
// Use pItem (not shown).
}
// Clean up.
SafeRelease(&pItem);
SafeRelease(&pFileOpen);
return hr;
}
在此模式中,会在函数的末尾释放资源。 如果发生错误,当函数退出时,某些指针可能无效。 对无效指针调用 Release 将使程序崩溃 (或更糟) ,因此必须在释放它们之前初始化所有指向 NULL 的指针并检查它们是否为 NULL。 此示例使用 SafeRelease
函数;智能指针也是一个不错的选择。
如果使用此模式,则必须小心循环构造。 在循环中,如果任何调用失败,请从循环中中断。
优点
- 与“嵌套 ifs”模式相比,此模式创建的嵌套更少。
- 总体控制流更易于查看。
- 资源在代码中的某个时间点释放。
缺点
- 必须在函数顶部声明和初始化所有变量。
- 如果调用失败,函数会进行多次不必要的错误检查,而不是立即退出函数。
- 由于控制流在发生故障后会继续通过函数,因此必须在整个函数主体中小心,不要访问无效的资源。
- 循环中的错误需要特殊情况。
跳跃失败
每次调用方法后,测试失败 (不成功) 。 失败时,跳转到函数底部附近的标签。 在标签后面,但在退出函数之前,释放资源。
HRESULT ShowDialog()
{
IFileOpenDialog *pFileOpen = NULL;
IShellItem *pItem = NULL;
HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
if (FAILED(hr))
{
goto done;
}
hr = pFileOpen->Show(NULL);
if (FAILED(hr))
{
goto done;
}
hr = pFileOpen->GetResult(&pItem);
if (FAILED(hr))
{
goto done;
}
// Use pItem (not shown).
done:
// Clean up.
SafeRelease(&pItem);
SafeRelease(&pFileOpen);
return hr;
}
优点
- 总体控制流易于查看。
- 在失败检查后代码中的每一个点,如果尚未跳转到标签,可以保证以前的所有调用都成功。
- 资源在代码中的一个位置发布。
缺点
- 必须在函数顶部声明和初始化所有变量。
- 一些程序员不喜欢在代码中使用 goto 。 (但是,应当指出, goto 的这种使用是高度结构化的:代码永远不会跳转到当前函数 call.)
- goto 语句跳过初始值设定项。
失败时引发
当方法失败时,可以引发异常,而不是跳转到标签。 如果你习惯于编写异常安全代码,这会产生更惯用的 C++ 样式。
#include <comdef.h> // Declares _com_error
inline void throw_if_fail(HRESULT hr)
{
if (FAILED(hr))
{
throw _com_error(hr);
}
}
void ShowDialog()
{
try
{
CComPtr<IFileOpenDialog> pFileOpen;
throw_if_fail(CoCreateInstance(__uuidof(FileOpenDialog), NULL,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen)));
throw_if_fail(pFileOpen->Show(NULL));
CComPtr<IShellItem> pItem;
throw_if_fail(pFileOpen->GetResult(&pItem));
// Use pItem (not shown).
}
catch (_com_error err)
{
// Handle error.
}
}
请注意,此示例使用 CComPtr 类来管理接口指针。 通常,如果代码引发异常,则应遵循 RAII (资源获取是初始化) 模式。 也就是说,每个资源都应由其析构函数保证资源正确释放的对象进行管理。 如果引发异常,则保证调用析构函数。 否则,程序可能会泄漏资源。
优点
- 与使用异常处理的现有代码兼容。
- 与引发异常的 C++ 库兼容,例如标准模板库 (STL) 。
缺点
- 需要 C++ 对象来管理资源,例如内存或文件句柄。
- 需要很好地了解如何编写异常安全代码。
下一步
反馈
https://aka.ms/ContentUserFeedback。
即将发布:在整个 2024 年,我们将逐步淘汰作为内容反馈机制的“GitHub 问题”,并将其取代为新的反馈系统。 有关详细信息,请参阅:提交和查看相关反馈