Windows桌面应用程序(1-2-3-10th) COM中的错误处理

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; 但不要让这个事实误导你。 一种方法可能会返回其他成功代码,因此请始终使用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来粗略地表示不是失败的否定条件。 它也可以表示“无操作” - 方法成功,但没有效果。 例如,如果您从同一个线程第二次调用CoInitializeEx函数,则会返回S_FALSE。 如果您需要在代码中区分S_OK和S_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指针。
  • 发布后不要尝试使用资源。

考虑到这些规则,这里有四种处理错误的模式。

  • 嵌套if
  • 连续if
  • goto
  • throw

嵌套if

每次返回HRESULT的调用后,使用if语句测试是否成功。然后,将下一个方法调用放在if语句的范围内。更多if语句可以根据需要嵌套。此模块中的先前代码示例都使用了此模式,但在此处再次出现:

HRESULT ShowDialog(){
    IFileOpenDialog *pFileOpen;
    HRESULT hr=CoCreateInstance(__uuidof(FileOpenDialog),NULL,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&amp,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语句时,已知pItempFileOpen都是有效的。
  • 很明显何时发布接口指针和其他资源。您在紧跟在获取资源的调用之后的if语句末尾释放资源。

缺点

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

连续if

在每次调用方法之后,使用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函数;智能指针也是不错的选择。

如果使用此模式,则必须小心循环结构。在循环内部,如果任何调用失败,则从循环中断。

优点

  • 此模式比“嵌套if”模式创建的嵌套更少。
  • 整体控制流程更容易看到。
  • 资源在代码中的某一点发布。

缺点

  • 必须在函数顶部声明和初始化所有变量。
  • 如果调用失败,该函数会进行多次不需要的错误检查,而不是立即退出该函数。
  • 由于控制流程在故障后继续通过该功能,因此您必须在整个功能体内小心不要访问无效资源。
  • 循环内部的错误需要特殊情况。

goto

每次调用方法后,测试失败(不成功)。失败时,跳转到功能底部附近的标签。标签之后,但在退出该功能之前,释放资源。

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语句跳过初始值设定项。

throw

不跳转到标签,您可以在方法失败时抛出异常。 如果您习惯于编写异常安全的代码,这可以产生更加惯用的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图形


原文链接:Error Handling in COM

返回目录

猜你喜欢

转载自blog.csdn.net/qq_37422196/article/details/79771927
今日推荐