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错误代码,这意味着程序使用的像素格式不受支持。 MSDN 文档通常提供方法可能返回的特定错误代码列表。 但是,不应认为这些列表是明确的。 方法始终可以返回文档中未列出的 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 块内。 但是,不要进一步嵌套 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++ 对象来管理资源,例如内存或文件句柄。
  • 需要很好地了解如何编写异常安全代码。

下一步

模块 3. Windows 图形