Using the REFIID/void** pattern for returning COM objects for future-proofing and to avoid problems with dependencies

Raymond Chen

Suppose you have a function that creates a reference to a COM object:

// pixie.h
STDAPI CreateShellItemFromPixieDust(
    const PIXIEDUST *ppd,
    IShellItem **ppsi);

There are a few issues with this design.

First of all, it requires that whoever uses your header filemust have included shlobj.h first,since that’s where IShell­Item is defined.You could solve that problem by putting#include <shlobj.h> at the top ofpixie.h,but that creates its own problems.For example, many header files alter their behavior basedon symbols that have been #defined,and including that other header file as part ofpixie.h means that it’s going to use the settingsthat were active at the time pixie.h was included(which may not be what the clients of your header file are expecting).For example:

#include <windows.h>
#include <ole2.h>
#include <pixie.h>
#define STRICT_TYPED_ITEMIDS
#include <shlobj.h>

This program wants to use strict typed item IDs,so it defines the magic symbol before includingshlobj.h.Unfortunately, that request is ignored becausethe pixie.h header file secretly includedshlobj.h prematurely.

This can get particularly messy if somebody wants toinclude shlobj.h with particular preprocessortricks temporarily active.

#include <windows.h>
#include <ole2.h>
#include <pixie.h>
// The WINDOWDATA structure added in Windows Vista conflicts
// with the one we defined back in 2000, so rename it to
// WINDOWDATA_WINDOWS.
#define WINDOWDATA WINDOWDATA_WINDOWS
#include <shlobj.h>
#undef WINDOWDATA
// Here's our version of WINDOWDATA
#include <contosotypes.h>

This code works around a naming conflict that was createdwhen Windows Vista added a structure calledWINDOW­DATA to shlobj.h.The application already had a structure with the same name,so it has to rename the one in shlobj.h tosome other name to avoid a redefinition error.

If you made pixie.h includeshlobj.h on its own,it would do so without this fancy renaming, and thedevelopers of that code will curse and say something like this:

#include <windows.h>
#include <ole2.h>
// The WINDOWDATA structure added in Windows Vista conflicts
// with the one we defined back in 2000, so rename it to
// WINDOWDATA_WINDOWS.
#define WINDOWDATA WINDOWDATA_WINDOWS
// pixie.h secretly includes shlobj.h so we have to put its #include
// under WINDOWDATA protection.
#include <pixie.h>
#include <shlobj.h>
#undef WINDOWDATA
// Here's our version of WINDOWDATA
#include <contosotypes.h>

Another problem with theCreate­Shell­Item­From­Pixie­Dustfunction is that it hard-codes the output interface toIShell­Item.When everybody moves on toIShell­Item2,all the callers will have tofollow theCreate­Shell­Item­From­Pixie­Dustcall with a Query­Interface to get the interfacethey really want.(Which, if your object is out-of-process,could mean another round trip to the server.)

The solution to both of these problems is to simply make thecaller specify what type of object they want.

// pixie.h
STDAPI CreateShellItemFromPixieDust(
    const PIXIEDUST *ppd,
    REFIID riid,
    void **ppv);

Now that we are no longer mentioningIShell­Item explicitly,we don’t need to include shlobj.h any more.And if the caller wants IShell­Item2,they can just ask for it.

Your creation function used to look like this:

STDAPI CreateShellItemFromPixieDust(
    const PIXIEDUST *ppd,
    IShellItem **ppsi)
{
    *ppsi = nullptr;
    IShellItem *psiResult;
    HRESULT hr = ... do whatever ...;
    if (SUCCEEDED(hr))
    {
       *ppsi = psiResult;
    }
    return hr;
}

You simply have to tweak the way you return the pointer:

STDAPI CreateShellItemFromPixieDust(
    const PIXIEDUST *ppd,
    REFIID riid,
    void **ppv)
{
    *ppv = nullptr;
    IShellItem *psiResult;
    HRESULT hr = ... do whatever ...;
    if (SUCCEEDED(hr))
    {
       hr = psiResult->QueryInterface(riid, ppv);
       psiResult->Release();
    }
    return hr;
}

Callers of your function would go from

IShellItem *psi;
hr = CreateShellItemFromPixieDust(ppd, &psi);

to

IShellItem *psi;
hr = CreateShellItemFromPixieDust(ppd, IID_PPV_ARGS(&psi));

If the caller decides that they really want anIShell­Item2,they merely have to change their variable declaration;the call to the creation function is unchanged.

IShellItem2 *psi;
hr = CreateShellItemFromPixieDust(ppd, IID_PPV_ARGS(&psi));