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来粗略地表示不是失败的否定条件。 它也可以表示“无操作” - 方法成功,但没有效果。 例如,如果您从同一个线程第二次调用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指针。
- 发布后不要尝试使用资源。
考虑到这些规则,这里有四种处理错误的模式。
- 嵌套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(&,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语句末尾释放资源。
缺点
- 有些人发现深层筑巢难以阅读。
- 错误处理与其他分支和循环语句混合在一起。这可能使整个程序逻辑更难以遵循。
连续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 ++对象来管理内存或文件句柄等资源。
- 需要很好地理解如何编写异常安全的代码。
下一个
返回目录