The Malloc1 and Malloc2 Samples

Let's now use CoGetMalloc and the IMalloc interface to illustrate the differences between C and C++ interface calls. The samples of interest here are in CHAP02\MALLOC1 and CHAP02\MALLOC2. Malloc1 is written in C and Malloc2 in C++, serving as side-by-side examples of equivalent functionality and equivalent code structures. Both samples call CoInitialize and CoUninitialize as necessary and create a small main window with a menu from which you can access the allocator, ask it to perform various functions, release the allocator, and exit the program. Both allocate a number of consecutively larger blocks (from 100 bytes up to tens of thousands of bytes), storing the ASCII character a in the first block, b in the second, and so forth. You can use a debugging tool to look at memory and see where the allocations ended up.

Both samples call CoGetMalloc when you select the Allocator/CoGetMalloc menu item. They save the pointer in a variable named m_pIMalloc, which is part of the application structure (in C) or the C++ object. When you select the Allocator/Alloc or Realloc menu item, you'll end up in the IDM_ALLOC (or IDM_REALLOC) case of each sample's window procedure, which verifies that you've selected CoGetMalloc already and calls a DoAllocation function. Malloc1's C code for this is in App_DoAllocations, which appears as follows:


BOOL App_DoAllocations(PAPP pApp, BOOL fRealloc)
{
UINT i;
ULONG iByte;
BOOL fResult=TRUE;
ULONG cb;
LPVOID pv;

if (!fRealloc)
App_FreeAllocations(pApp, FALSE);

for (i=0; i < CALLOCS; i++)
{
//cb is set in the code below for later initialization.
if (fRealloc)
{
pApp->m_rgcb[i]+=128;
cb=pApp->m_rgcb[i];

//Old memory is not freed if Realloc fails.
pv=pApp->m_pIMalloc->lpVtbl->Realloc(pApp->m_pIMalloc
, pApp->m_rgpv[i], cb);
}
else
{
cb=pApp->m_rgcb[i];
pv=pApp->m_pIMalloc->lpVtbl->Alloc(pApp->m_pIMalloc, cb);
}

pApp->m_rgpv[i]=pv;

[Code to fill allocations with letters]
fResult &= (NULL!=pv);
}

[Other cleanup code]
return fResult;
}

Here's the equivalent code from Malloc2's (MALLOC.CPP) CApp::DoAllocations:


BOOL CApp::DoAllocations(BOOL fRealloc)
{
UINT i;
ULONG iByte;
BOOL fResult=TRUE;
ULONG cb;
LPVOID pv;

if (!fRealloc)
FreeAllocations(FALSE);

for (i=0; i < CALLOCS; i++)
{
//cb is set in the code below for later initialization.
if (fRealloc)
{
m_rgcb[i]+=128;
cb=m_rgcb[i];

//Old memory is not freed if Realloc fails.
pv=m_pIMalloc->Realloc(m_rgpv[i], cb);
}
else
{
cb=m_rgcb[i];
pv=m_pIMalloc->Alloc(cb);
}

m_rgpv[i]=pv;

[Code to fill allocations with letters]
fResult &= (NULL!=pv);
}

[Other cleanup code]
return fResult;
}

In each case, the array m_rgcb contains a bunch of integer values to determine the size of the allocations. Each Realloc operation increases the size. The array m_rgpv holds the allocated pointers that are freed in the samples' FreeAllocations functions.

To closely compare C and C++ function calls, look at the calls to Alloc:


C:    pv=pApp->m_pIMalloc->lpVtbl->Alloc(pApp->m_pIMalloc, cb);
C++: pv=m_pIMalloc->Alloc(cb);

In C++, you need to call only the function through the interface pointer using a single indirection, as you would when making a member function call to any C++ object. This is because OLE interfaces are designed to have the same structure as C++ objects have in memory, and because interfaces are defined in C++ abstract base classes, the compiler hides all the complexities of making the call. In C, however, these complexities are explicit: you must state the double indirection to the function using the lpVtbl field of the interface structure, and you must pass the interface pointer itself as the first parameter. C++ performs this last step automatically to generate the this pointer inside the member function.

In even a simple client implementation, you will easily have 50 calls to interface member functions. You don't even need to multiply to see why C++ is the more convenient language in which to work with OLE, although it is no more functional. If you are using other languages, the compiler or programming environment should support some sort of language construct that represents interfaces and making calls to interfaces.

If you want to take advantage of the convenience of C++ but don't want to rewrite your existing C code, I have a suggestion. Simply rename your C files to CPP and recompile. Three weeks later, when you've added the necessary extern "C" statements around include files and have fixed all the type warnings that you'll get, you effectively have C++ code. Remember that C is a subset of C++. You can use this fact to your advantage to make the compiler understand that you are writing C++ code so that when you use an interface pointer you can benefit from the more concise calling structure. Investing some time up front to recompile your code as C++ is, believe me, well worth it. I've written code both ways, and it gets painful to write lpVtbl all over the place. If you really can't recompile your code as C++, try making sets of macros for each interface that will turn an interface call into MACRO(<interface pointer>, <arguments>);. It will make your source code smaller, your code more readable, and your programmers happier.