COM 编码做法
本主题介绍使 COM 代码更高效、更可靠的方法。
__uuidof 运算符
生成程序时,可能会收到类似于以下内容的链接器错误:
unresolved external symbol "struct _GUID const IID_IDrawable"
此错误表示 GUID 常量是使用外部链接(外部链接)声明的,并且链接器找不到常量的定义。 GUID 常量的值通常从静态库文件导出。 如果使用 Microsoft Visual C++,则可以避免使用 __uuidof 运算符链接静态库。 此运算符是Microsoft语言扩展。 它从表达式返回 GUID 值。 表达式可以是接口类型名称、类名或接口指针。 使用 __uuidof,可以创建通用项对话框对象,如下所示:
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL,
__uuidof(pFileOpen), reinterpret_cast<void**>(&pFileOpen));
编译器从标头中提取 GUID 值,因此不需要库导出。
注意
GUID 值通过声明标头中的 __declspec(uuid( ... ))
来与类型名称相关联。 有关详细信息,请参阅 Visual C++ 文档中 __declspec 的文档。
IID_PPV_ARGS宏
我们看到,CoCreateInstance 和 QueryInterface 都需要将最终参数强制转换为 void** 类型。 这会创建类型不匹配的可能性。 请考虑以下代码片段:
// Wrong!
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(
__uuidof(FileOpenDialog),
NULL,
CLSCTX_ALL,
__uuidof(IFileDialogCustomize), // The IID does not match the pointer type!
reinterpret_cast<void**>(&pFileOpen) // Coerce to void**.
);
此代码要求 IFileDialogCustomize 接口,但传入 IFileOpenDialog 指针。 reinterpret_cast 表达式绕过C++类型系统,因此编译器不会捕获此错误。 在最佳情况下,如果对象未实现请求的接口,则调用只会失败。 在最坏的情况下,函数成功,并且指针不匹配。 换句话说,指针类型与内存中的实际 vtable 不匹配。 正如你想象的,在这一点上,没有什么好处会发生。
注意
vtable(虚拟方法表)是函数指针表。 vtable 是 COM 如何在运行时将方法调用绑定到其实现。 并非巧合的是,vtable 是大多数C++编译器实现虚拟方法的方式。
IID_PPV_ARGS 宏有助于避免此类错误。 若要使用此宏,请替换以下代码:
__uuidof(IFileDialogCustomize), reinterpret_cast<void**>(&pFileOpen)
替换为:
IID_PPV_ARGS(&pFileOpen)
宏会自动为接口标识符插入 __uuidof(IFileOpenDialog)
,因此可以保证与指针类型匹配。 下面是修改的(正确)代码:
// Right.
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL,
IID_PPV_ARGS(&pFileOpen));
可以将同一宏用于 QueryInterface:
IFileDialogCustomize *pCustom;
hr = pFileOpen->QueryInterface(IID_PPV_ARGS(&pCustom));
SafeRelease 模式
引用计数是编程中的一项,它基本上很容易,但也繁琐,这使得很容易出错。 典型错误包括:
- 使用完接口指针后,无法释放接口指针。 此类 bug 将导致程序泄漏内存和其他资源,因为对象不会销毁。
- 使用无效指针调用 Release。 例如,如果从未创建对象,则可能发生此错误。 此类 bug 可能会导致程序崩溃。
- 调用 发布 后取消引用接口指针。 此 bug 可能会导致程序崩溃。 更糟的是,它可能会导致程序在以后随机崩溃,使得很难跟踪原始错误。
避免这些 bug 的一种方法是通过安全地释放指针的函数调用 Release。 以下代码显示了一个执行此作的函数:
template <class T> void SafeRelease(T **ppT)
{
if (*ppT)
{
(*ppT)->Release();
*ppT = NULL;
}
}
此函数采用 COM 接口指针作为参数,并执行以下作:
- 检查指针是否 NULL。
- 如果指针未 NULL,则调用 Release。
- 将指针设置为 NULL。
下面是如何使用 SafeRelease
的示例:
void UseSafeRelease()
{
IFileOpenDialog *pFileOpen = NULL;
HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
if (SUCCEEDED(hr))
{
// Use the object.
}
SafeRelease(&pFileOpen);
}
如果 CoCreateInstance 成功,则调用 SafeRelease
释放指针。 如果 CoCreateInstance 失败,pFileOpen 将保持 NULL。
SafeRelease
函数会检查此情况,并跳过对 Release的调用。
也可以安全地在同一指针上多次调用 SafeRelease
,如下所示:
// Redundant, but OK.
SafeRelease(&pFileOpen);
SafeRelease(&pFileOpen);
COM 智能指针
SafeRelease
函数非常有用,但它需要记住两件事:
- 初始化每个接口指针,以 NULL。
- 在每个指针超出范围之前调用
SafeRelease
。
作为一名C++程序员,你可能认为你不应该记住这两件事。 毕竟,这就是C++具有构造函数和析构函数的原因。 最好有一个类包装基础接口指针,并自动初始化和释放指针。 换句话说,我们希望如下所示:
// Warning: This example is not complete.
template <class T>
class SmartPointer
{
T* ptr;
public:
SmartPointer(T *p) : ptr(p) { }
~SmartPointer()
{
if (ptr) { ptr->Release(); }
}
};
此处显示的类定义不完整,不可用,如下所示。 至少需要定义复制构造函数、赋值运算符和访问基础 COM 指针的方法。 幸运的是,无需执行任何此作,因为Microsoft Visual Studio 已提供智能指针类作为活动模板库(ATL)的一部分。
ATL 智能指针类 CComPtr命名。 (还有一个 CComQIPtr 类,此处未讨论。下面是 打开对话框 示例重写,以使用 CComPtr。
#include <windows.h>
#include <shobjidl.h>
#include <atlbase.h> // Contains the declaration of CComPtr.
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED |
COINIT_DISABLE_OLE1DDE);
if (SUCCEEDED(hr))
{
CComPtr<IFileOpenDialog> pFileOpen;
// Create the FileOpenDialog object.
hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));
if (SUCCEEDED(hr))
{
// Show the Open dialog box.
hr = pFileOpen->Show(NULL);
// Get the file name from the dialog box.
if (SUCCEEDED(hr))
{
CComPtr<IShellItem> pItem;
hr = pFileOpen->GetResult(&pItem);
if (SUCCEEDED(hr))
{
PWSTR pszFilePath;
hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);
// Display the file name to the user.
if (SUCCEEDED(hr))
{
MessageBox(NULL, pszFilePath, L"File Path", MB_OK);
CoTaskMemFree(pszFilePath);
}
}
// pItem goes out of scope.
}
// pFileOpen goes out of scope.
}
CoUninitialize();
}
return 0;
}
此代码与原始示例的主要区别在于,此版本不会显式调用 Release。 当 CComPtr 实例超出范围时,析构函数会在基础指针上调用 Release。
CComPtr 是类模板。 模板参数是 COM 接口类型。 在内部,CComPtr 保存该类型的指针。 CComPtr 重写 运算符>() 和 运算符&(),使类像基础指针一样。 例如,以下代码等效于直接调用 IFileOpenDialog::Show 方法:
hr = pFileOpen->Show(NULL);
CComPtr 还定义了 CComPtr::CoCreateInstance 方法,该方法使用一些默认参数值调用 COM CoCreateInstance 函数。 唯一必需的参数是类标识符,如下例所示:
hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));
CComPtr::CoCreateInstance 方法纯粹作为便利提供;如果愿意,仍然可以调用 COM CoCreateInstance 函数。
下一个
COM 中的 错误处理