COM 组件中使用 HRESULT 类型的值表示方法或者函数调用的结果成功与否。各种各样的 SDK 在头文件中定义了很多不同的 HRESULT 类型的常量。一个通用的代码在 WinError.h 中定义:

常量 描述
E_ACCESSDENIED 0x80070005 Access denied.
E_FAIL 0x80004005 Unspecified error.
E_INVALIDARG 0x80070057 Invalid parameter value.
E_OUTOFMEMORY 0x8007000E Out of memory.
E_POINTER 0x80004003 NULL was passed incorrectly for a pointer value.
E_UNEXPECTED 0x8000FFFF Unexpected condition.
S_OK 0x0 Success.
S_FALSE 0x1 Success.

所有错误常量标识的前缀都是 “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 的含义,并不是代表执行失败,而是代表没有做任何操作的意思,这个方法是成功的,只不过没有产生任何影响。

例如,如果你在一个线程中多次调用 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");
}

在 Windows 系统中,一些函数会返回一些明确的失败信息。例如,Direct2D 图形 API 定义了一个错误码:D2DERR_UNSUPPORTED_PIXEL_FORMAT,它代表程序不支持的像素格式错误。MSDN 文档经常会给出一些常见错误列表,然而你不能依赖于文档中的错误列表,因为函数可能会返回一些文档中没有介绍的错误类型。请记住,始终使用 FAILED 或者 SUCCEEDED 宏来判断错误与否。如果你想测试返回值是否为具体的错误类型,书写的方式应该类似下面的代码:

if (hr == D2DERR_UNSUPPORTED_PIXEL_FORMAT)
{
    // Handle the specific case of an unsupported pixel format.
}
else if (FAILED(hr))
{
    // Handle other errors.
}

1. 常见的错误处理模式

本节将介绍一些处理 COM 错误 的常见模式方法。每种模式都有其优缺点。某种程度上,是一种个人品味问题。如果你在一个现有的工程项目上工作,可能已经有了类似的代码编程准则规范。不过不管你采取何种模式,健壮的代码都会有如下的规则:

  • 对于返回 HRESULT 类型的方法或者函数,在代码继续前请检查返回的结果。

  • 释放那些不再使用的资源。

  • 不要试图访问无效或者没有初始化的资源,例如 NULL 指针。

  • 不要尝试使用释放后的资源(指针)。

根据这些规则,产生下面这四种错误处理模式:

1.1 Nested 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 在被使用前一直没有声明。

  • 在每一个 if 语句作用域内,都代表着函数成功返回,在之前所有申请的资源都是有效的,例如,当程序运行到最里面的 if 语句时,pItem 和 pFileOpen 都是有效的。

  • 什么时候释放资源或者接口指针都非常明确清晰,一般都是在 if 语句的末尾释放。

劣势:

  • 嵌套过深,难以阅读。

  • 错误处理和分支语句或者循环语句混合在一起,它可能让程序的逻辑难以理解。

1.2 Cascading ifs

每次调用函数后,使用 if 语句判断是否成功。如果方法成功,将下一个函数调用放到 if 语句块中。但是每个 if 语句没有嵌套在一起,if 语句都是平行并列的关系,每个 if 语句都测试上个方法是否运行成功,如果有任何一个方法失败,剩余的所有 if 语句中的 SUCCESSED 宏条件测试都会返回失败,直到函数末尾。

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,这样在函数的最后可以使用 SafeRelease 函数进行资源释放操作。

如果你是用这种模式,请小心循环结构。在循环中,如果任何函数返回失败,都应该立即使用 break 跳出循环。

优点:

  • 这个模式嵌套很少。

  • 流程控制清晰可见。

  • 资源在某一时刻一起释放。

缺点:

  • 所有的变量都必须在函数的前面声明初始化。

  • 如果中间调用失败,这种模式可能会产生很多没有必要的错误检测逻辑,而不是立即退出。

  • 因为失败之后,仍然会执行一些逻辑判断操作,你必须小心避免访问那些无效的资源。

  • 循环语句的内部错误需要特殊处理。

1.3 Jump on Fail

在每个函数调用后,测试是否成功,如果失败则跳转到底部的错误处理标签。标签后的代码在函数退出之前负责释放资源。

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 的语句时高度结构化的,并不会跳出当前函数)。

  • 可能会产生 goto 语句跳过初始化现象。

1.4 Throw on Fail

当函数失败的时候,你可以不使用跳转标签的方法,而是用异常。如果你习惯于 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(Resource Acquisition is Initialization)模式。换句话说,每个资源都应该由一个对象管理,对象销毁会自动调用析构函数释放资源。如果函数内部出现错误抛出异常,对象的析构函数会被调用,进而确保资源可以正确释放。否则,代码中可能会导致资源泄露。

优点:

  • 与使用现代代码相兼容。

  • 与抛出异常的 C++ 库相兼容,例如 STL。

缺点:

  • 需要用 C++ 对象来管理资源,例如内存、句柄等资源类型。

  • 需要对书写异常安全的代码有一定深入的理解。

求关注