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宏

我们看到,CoCreateInstanceQueryInterface 都需要将最终参数强制转换为 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 接口指针作为参数,并执行以下作:

  1. 检查指针是否 NULL
  2. 如果指针未 NULL,则调用 Release
  3. 将指针设置为 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 将保持 NULLSafeRelease 函数会检查此情况,并跳过对 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 中的 错误处理