1. __uuidof 运算符

在编译程序的时候,你可能会看到类似下面的链接错误:

unresolved external symbol "struct _GUID const IID_IDrawable"

这个错误意味着 GUID 常量被声明为外部链接(extern),这个链接没有找到这个常量的定义。这个 GUID 常量通常从一个静态库文件导入,如果你使用的是 Microsoft Visual C++,你可以通过使用 __uuidof 操作符避免链接一个静态库。这个操作符是微软独有的一个语言扩展标记,它从表达式返回一个 GUID 值。这个表达式可以是一个接口类型的名称,一个类的名称或者是一个接口指针。使用 __uuidof,你能用下面的方法创建 Common Item Dialog 对象。

IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL,
    __uuidof(pFileOpen), reinterpret_cast<void**>(&pFileOpen));

编译器会自动从头部中提取 GUID 值,所以无需从库中导出。

这个 GUID 值和头部类型声明的语句 __declspec(uuid( …… )) 息息相关的,__declspec 更多的信息可以通过 Visual C++ 的文档查看。

2. 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++ 类型系统的检测,所以编译器并不会主动抛出错误。

如果这段代码被运行,好的结果是函数返回失败,无法找到该接口;而坏的结果是,函数会返回成功,而你将获得一个类型结果不匹配的指针。换句话说,这个指针类型没有匹配到内存中真实的虚函数表,如果你所想不会有啥好事发生。

一个虚函数表(virtual method table)是一个函数指针表,它被用来实现 COM 组件运行期间的动态绑定。这种方式也是大多数 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));

3. SafeRelease 模式

引用计数是编程中最简单的一种方式,但也很单调乏味,并且非常容易产生错误,例如:

  • 使用完毕后没有释放接口指针。这个 bug 可能会导致你程序的内存泄露或者其他资源没有及时释放,因为对象始终没有被销毁。

  • 释放(Release)一个无效的接口指针。这个错误常见于一个对象没有创建成功的情况下,这个错误会导致你程序的崩溃。

  • 使用一个已释放(Release)的接口指针。这个 bug 可能也会导致你程序崩溃。更悲剧的是,使用一个已释放的接口未必每次都会让你的程序崩溃,具体决定于操作系统何时将这块内存再次利用起来,如果发生错误会很难跟踪定位错误的具体位置。

有一种方法避免这些 bug,让你安全释放现有的指针,代码如下:

template <class T> void SafeRelease(T **ppT)
{
    if (*ppT)
    {
        (*ppT)->Release();
        *ppT = NULL;
    }
}

这个函数接收一个 COM 接口 指针并执行以下操作:

  1. 检测指针是否为 NULL。

  2. 如果指针非空,调用 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 为 NULL,SafeRelease 函数检测到指针为 NULL,会跳过 Release 操作。

下面这段代码是安全的:

// Redundant, but OK.
SafeRelease(&pFileOpen);
SafeRelease(&pFileOpen);

4. COM 智能指针

SafeRelease 函数是非常有用的,但是你需要记住两点:

  • 将每个接口指针初始化为 NULL。

  • 每个指针在离开作用域之前都需要调用 SafeRelease。

作为一个 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 指针的潜在方式。幸运的是,你不需要重复做这些工作,微软的 Visual Studio 已经提供了一种智能指针,它是活动模板库(ATL)的一部分。

ATL 的智能指针名字叫做 CComPtr,下面是使用 CComPtr 重写 Open Dialog Box 案例的代码:

#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 重载了 operator->() 和 operator&() 两个操作符,让它们在使用起来的时候和原始指针的操作相同。例如,下面这段代码和前面直接调用 IFileOpenDialog::Show 方法是等价的。

hr = pFileOpen->Show(NULL);

CComPtr 也定义了一个叫做 CComPtr::CoCreateInstance 的方法。它内部调用了 COM 的 CoCreateInstance 函数和一些默认参数。函数参数仅仅需要一个类标识即可,使用如下:

hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));

CComPtr::CoCreateInstance 这个方法仅仅是为了使用方便,如果愿意仍然可以直接调用 COM 的 CoCreateInstance 函数。