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_OKS_FALSE 都是成功代码。 可能 99% 的 COM 方法在成功时返回 S_OK;但不要让这一事实误导你。 方法可能会返回其他成功代码,因此始终使用 SUCCEEDEDFAILED 宏测试错误。 下面的示例代码展示了测试函数调用成功与否的错误和正确方法。

// 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_OKS_FALSE,则应直接测试该值,但仍使用 FAILEDSUCCEEDED 来处理其余情况,如以下示例代码所示。

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,这意味着程序使用了不受支持的像素格式。 Windows 文档通常提供某个方法可能返回的特定错误代码的列表。 但是,您不应将这些清单视为定论。 方法始终可以返回文档中未列出的 HRESULT 值。 同样,使用 SUCCEEDEDFAILED 宏。 如果要测试特定的错误代码,也要包括默认用例。

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 语句时,已知 pItempFileOpen 都是有效的。
  • 何时释放接口指针和其他资源非常明确。 在紧跟获取资源的调用之后的 if 语句末尾释放资源。

缺点

  • 有些人发现深嵌套难以阅读。
  • 错误处理与其他分支和循环语句混合在一起。 这会使整个程序逻辑更难遵循。

级联 ifs

在每个方法调用后,使用 if 语句测试是否成功。 如果方法成功,请将下一个方法调用置于 if block 内。 但是,不要嵌套 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;
}

优点

  • 整体控制流易于查看。
  • FAILED 检查后代码的每一个点,如果尚未跳转到标签,则可以保证所有以前的调用都已成功。
  • 资源在代码的一个位置发布。

缺点

  • 必须在函数顶部声明和初始化所有变量。
  • 一些程序员不喜欢在代码中使用 goto 。 (但是,需要注意的事,这种 goto 的使用高度结构化;代码永远不会跳出当前函数调用。)
  • 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 类管理 COM 接口指针。 通常,如果代码引发异常,则应遵循 RAII(资源获取是初始化)模式。 也就是说,每个资源都应由析构函数保证资源正确释放的对象进行管理。 如果引发异常,则保证调用析构函数。 否则,程序可能会泄露资源。

优点

  • 与使用异常处理的现有代码兼容。
  • 与引发异常的C++库兼容,例如标准模板库 (STL)。

缺点

  • 需要 C++ 对象来管理内存或文件句柄等资源。
  • 需要很好地了解如何编写异常安全代码。

下一页

模块 3. Windows Graphics