Bruce McKinney
April 18. 1996
Although Microsoft® Visual C++® supports arrays intrinsically, it does so with no index protection, no size limit, no initialization—just a pointer to random-value memory. Even C++ programmers are reluctant to use raw arrays. Many of them write protected wrapper classes with names like Array or Vector. You can make such classes look and act like an array—but one protected by armor.
If you're going to make arrays available across process, machine, and operating system boundaries, your clients will expect more protection than a raw C++ array can provide. The OLE way of doing arrays (which is exactly the same as the Visual Basic® way) is through a protected data structure called a SAFEARRAY.
Because the OLE header files are implemented for both C and C++ (although using C is cumbersome, so this article will examine only the C++ case), they provide the protected standard array as a SAFEARRAY structure with a group of system functions that work on it.
When converted to C++ and trimmed of excess typedefs and conditionals, the SAFEARRAY structure looks something like this:
struct SAFEARRAY {
WORD cDims;
WORD fFeatures;
DWORD cbElements;
DWORD cLocks;
void * pvData;
SAFEARRAYBOUND rgsabound[1];
};
The last field is an array of boundary structures. By default, there's only one of these, but if you define multiple dimensions, the appropriate system function will reallocate the array to give you as many array elements as you need. The dimension array is the last member of the array so that it can expand. A SAFEARRAYBOUND structure looks like this:
struct SAFEARRAYBOUND {
DWORD cElements;
LONG lLbound;
};
The cElements field has the number of elements in the dimension, and the lLBound field has the lower boundary. In theory, you could define a range either by giving the first and last element, or by giving the first element and the number of elements. OLE chose the second format, but we'll fix that shortly.
Many of the system functions tend to work together in groups, so we'll talk about related functions in logical order. My descriptions of these functions are sometimes different from (and sometimes more complete than) the descriptions in OLE documentation. Because I personally tested them to determine some behavior that was sparsely documented, I am confident that my descriptions are valid and will work for you.
As long as you're working with single-dimension arrays, the SAFEARRAY functions are simple and straightforward. Things get more complicated with multi-dimensional arrays, partly because Visual Basic and Visual C++ have a different ideas of how to arrange the data in different dimensions. Also C++ wants to access zero-based arrays, but the SAFEARRAY type can have index boundaries based on any signed number.
The TestSA function in Test.Cpp gives the SafeArray system functions a workout. The examples for the functions described below are taken from this function, and the event handler for the SAFEARRAY button in the Cpp4VB sample program calls this function. I'm not going to go through the code in detail, but I will say that the more you study the raw functions, the more you'll appreciate the SafeArray class shown later.
One note on terminology: OLE documentation calls a pointer to an allocated SAFEARRAY structure an array descriptor. Note that an array descriptor isn't necessarily the same as a pointer to a SAFEARRAY structure. The structure has space for only one dimension (SAFEARRAYBOUND structure). A descriptor is expanded to provide additional memory for each dimension.
You create an array by calling SafeArrayCreate, passing it the type of the array in the vt parameter, the number of dimensions in the cDims parameter, and the size of each dimension in the adims parameter (an array of SAFEARRYBOUND structures). SafeArrayCreate creates a new array, allocates and initializes the data for the array, and returns a pointer to the SAFEARRAY structure. When you're done with the array, call SafeArrayDestroy. The advantage of these functions is that they're simple. The disadvantage is that they can only handle the OLE variant subtypes (excluding VT_ARRAY, VT_BYREF, VT_EMPTY, or VT_NULL). That's not really a disadvantage when you're dealing with Visual Basic or most other OLE clients.
When you destroy an array of BSTRs, VARIANTs, or objects with SafeArrayDestroy, BSTRs and VARIANTs are freed and objects are released.
Example:
// Create a new 1-D array of Integers.
SAFEARRAY * psaiNew;
SAFEARRAYBOUND aDim[1];
aDim[0].lLbound = 1;
aDim[0].cElements = 8;
// Equivalent to: Dim aiNew(1 To 8) As Integer.
psaiNew = SafeArrayCreate(VT_I2, 1, aDim);
if (psaiNew == NULL) throw ERROR_NOT_ENOUGH_MEMORY;
.
. // Use array.
.
if (hres = SafeArrayDestroy(psaiNew)) throw hres;
These functions provide more complicated and flexible alternative to SafeArrayCreate and SafeArrayDestroy. You can put non-OLE types into the array, but you must manage the data yourself. In this series of articles, we have no reason to put nonstandard data types into arrays. Visual Basic wouldn't know what to do with them anyway.
These functions insert or extract a single array element. You pass one of these functions an array pointer and an array of indexes for the element you want to access. It returns a pointer to a single element through the pvElem parameter. You also need to know the number of dimensions and supply an index array of the right size. The rightmost (least significant) dimension should be aiIndex[0] and the leftmost dimension should be aiIndex[psa->cDims-1]. These functions automatically call SafeArrayLock and SafeArrayUnlock before and after accessing the element. If the data element is a BSTR, VARIANT, or object, it is copied correctly with the appropriate reference counting or allocation. During an assignment, if the existing element is a BSTR, VARIANT, or object, it is cleared correctly, with the appropriate release or free before the new element is inserted. You can have multiple locks on an array, so it's OK to use these functions while the array is locked by other operations.
Example:
// Modify 2-D array with SafeArrayGetElement and SafeArrayGetElement.
long ai[2];
Integer iVal;
xMin = aDims[0].lLbound;
xMax = xMin + (int)aDims[0].cElements - 1;
yMin = aDims[1].lLbound;
yMax = yMin + (int)aDims[1].cElements - 1;
for (x = xMin; x <= xMax; x++) {
ai[0] = x;
for (y = yMin; y <= yMax; y++) {
ai[1] = y;
if (hres = SafeArrayGetElement(psaiInOut, ai, &iVal)) throw hres;
// Equivalent to: aiInOut(x, y) = aiInOut(x, y) + 1.
iVal++;
if (hres = SafeArrayPutElement(psaiInOut, ai, &iVal)) throw hres;
}
}
These functions increment or decrement the lock count of an array. The data becomes accessible through the pvData field of the array descriptor. The pointer in the array descriptor is valid until SafeArrayUnlock is called. Note that the pvData field, like all C++ arrays, is zero-indexed. If you need to keep track of the Basic index, initialize it from the lLbound field of the SAFEARRAYBOUND structure.
When processing data in a loop, it is more efficient to lock the array, process the data, and then unlock it, rather than making multiple calls to SafeArrayGetElement and SafeArrayPutElement. You can nest equal pairs of calls to SafeArrayLock and SafeArrayUnlock, so it's possible to lock and use an array while another operation also has a lock on the array. An array can't be deleted while it is locked.
Example:
// Initialize Integer array to squares of index.
if (hres = SafeArrayLock(psaiNew)) throw hres;
int iCur = aDim[0].lLbound;
// Keep separate C++ index (i) and Basic index (iCur).
for (i = 0; i < (int)aDim[0].cElements; i++, iCur++) {
// Equivalent to: ai(iCur) = iCur * iCur.
((Integer*)psaiNew->pvData)[i] = iCur * iCur;
}
if (hres = SafeArrayUnlock(psaiNew)) throw hres;
The example above illustrates accessing a simple 1-D array. As a bonus, here's an example of accessing a 2-D array without any help from SafeArrayPtrOfIndex. I had to consult a pack of C++ language lawyers (special credit to Paul Johns) for help untangling a type cast that looks sort of like a spilled can of night crawlers.
// Set up dimension array and pointer to receive value.
if (hres = SafeArrayLock(psaiInOut)) throw hres;
Integer (*aiInOut)[4] = (Integer(*)[4])psaiInOut->pvData;
for (x = 0; x < (int)aDims[0].cElements; x++) {
for (y = 0; y < (int)aDims[1].cElements; y++) {
// Equivalent to: aiInOut(x, y) = aiInOut(x, y) + 1.
// Switch x and y order for Visual Basic storage order.
aiInOut[y][x]++;
}
}
if (hres = SafeArrayUnlock(psaiInOut)) throw hres;
This function returns a pointer to an array element. You pass it an array of index values that identify an element of the array; it returns a pointer to the element. The array should be locked before SafeArrayPtrOfIndex is called. Use this function with multi-dimension arrays when using SafeArrayLock. For single-dimension arrays, it's usually easier to just index into the array directly without this function.
Example:
// Lock 2-D array and modify.
xMin = aDims[0].lLbound;
xMax = xMin + (int)aDims[0].cElements - 1;
yMin = aDims[1].lLbound;
yMax = yMin + (int)aDims[1].cElements - 1;
// Set up dimension array and pointer to receive value.
Integer * piInOut;
if (hres = SafeArrayLock(psaiInOut)) throw hres;
for (x = xMin; x <= xMax; x++) {
ai[0] = x;
for (y = yMin; y <= yMax; y++) {
ai[1] = y;
hres = SafeArrayPtrOfIndex(psaiInOut, ai, (void **)&piInOut);
if (hres) throw hres;
// Equivalent to: aiInOut(x, y) = aiInOut(x, y) + 1.
(*piInOut)++;
}
}
if (hres = SafeArrayUnlock(psaiInOut)) throw hres;
You pass SafeArrayAccessData a SAFEARRAY pointer and a variable to receive the address of the array data; it locks the array and returns a pointer to the data. When you're done, you call SafeArrayUnaccessData. This is the verbose equivalent of locking the data and using the pvData member out of the SAFEARRAY structure. It provides no way to calculate the index of multi-dimension arrays. I can't think of any reason to use these functions, so if you do, you're on your own.
This function creates a copy of an existing safe array: You pass it the descriptor of the array to copy and the address of a SAFEARRAY pointer that will receive the copy; it copies the source data to the destination. If the source array contains BSTR or VARIANT types, SafeArrayCopy calls the appropriate system functions to create the copies. If the source array contains object references, SafeArrayCopy increments their reference counts. You end up with two identical copies of the array.
Example:
// Copy from psaiNew to psaiRet.
SAFEARRAY * psaiRet;
if (hres = SafeArrayCopy(psaiNew, &psaiRet)) throw hres;
These functions return the number of dimensions in the array or the size in bytes of an element. They are equivalent to getting the corresponding elements out of the descriptor.
Example:
long cDim = SafeArrayGetDim(psaiInOut);
long cbElem = SafeArrayGetElemsize(psaiInOut);
These functions return the lower or upper boundary for any dimension of a safe array.
Example:
SAFEARRAYBOUND * aDims = new SAFEARRAYBOUND[cDim];
long iT;
for (i = 0; i < cDim; i++) {
hres = SafeArrayGetLBound(psaiInOut, i + 1, &aDims[i].lLbound);
if (hres) throw hres;
if (hres = SafeArrayGetUBound(psaiInOut, i + 1, &iT)) throw hres;
// Calculate elements from upper and lower boundaries.
aDims[i].cElements = iT - aDims[i].lLbound + 1;
}
This function changes the least significant (rightmost) bound of a safe array. You pass an array pointer to SafeArrayRedim and a pointer to a SAFEARRAYBOUND variable containing the desired dimensions. If you reduce the size of the array, SafeArrayRedim deallocates the array elements outside the new array boundary. If you increase the size, SafeArrayRedim allocates and initializes the new array elements. The data is preserved for elements that exist in both the old and the new array. To redimension an array passed from Visual Basic, the array must be a non-static array:
' Use SafeArrayRedim on this one.
Dim aiModify () As Integer
ReDim Preserve aiModify(1 To 8, 1 To 8) As Integer
' Don't use SafeArrayRedim.
Dim aiFixed(1 To 8, 1 To 8) As Integer
You can identify a fixed-length array passed from Visual Basic by the fFeatures field of the SAFEARRAY structure. Basic sized arrays declared with Dim, Private, or Public will have the FADF_STATIC and FADF_FIXEDSIZE flags. Arrays sized with Basic's ReDim statement (and thus usable with SafeArrayRedim) will not have these flags.
Example:
// Double the size of the last dimension.
i = cDim - 1;
aDims[i].cElements *= 2;
if (hres = SafeArrayRedim(psaiInOut, &aDims[i])) throw hres;
You're probably not used to thinking of an array as a type—it's more of a container for other types. But in C++ just about anything can be a type. Until recently, if you wanted to define one type that held objects of another type, you had to decide at design time what type you wanted to contain. You could design an ArrayLong type, but then you'd have to copy all your code and do a lot of searching and replacing to add an ArrayString type.
C++ templates now let you define an array type that can contain any kind of OLE type. You can declare an array of strings like this:
SafeArray<BSTR, VT_BSTR> as = Dim(1, 8); // Dim as(1 To 8) As String
You specify by what you put in the angle brackets exactly what type you want to be contained in the array. It gets a little tiresome, not to mention redundant, to specify both the type and the OLE type constant for every array, but typedefs make it easy to define the standard OLE types:
typedef SafeArray<BSTR, VT_BSTR> ArrayString;
Now you can define the string more naturally.
ArrayString as = Dim(1, 8); // Dim as(1 To 8) As String
The other OLE types have Basic-style names: ArrayByte, ArrayInteger, ArrayLong, ArraySingle, ArrayDouble, ArrayVariant, ArrayCurrency, ArrayDate, ArrayBoolean, and ArrayObject. The trick is that we don't have to define a separate class for each type. There's just one class with 11 predefined variations, and the ability to define more, although the predefined ones are all you need for a Visual Basic client.
You can see some of these array definitions in action in the TestSafeArray function in Test.Cpp. This function is tested by the event handler of the SafeArray button in the Cpp4VB sample program.
But before we look at things you might want to do with SafeArray types, what the heck is that Dim object being assigned to the array?
The SAFEARRAYBOUND type is the official way of specifying a dimension for a SAFEARRAY, but it's not my idea of how a dimension ought to be specified. It certainly doesn't look like a Visual Basic dimension, so I wrote a simpler, more friendly class called Dim. Dim is inherited from SAFEARRAYBOUND, so it has the same data members, but it also has friendly constructors and methods that make it easier to use. All methods of the Dim class are inline, so there's no penalty for using it.
Here's the entire class:
class Dim : public SAFEARRAYBOUND
{
public:
Dim(const long iLo, const long iHi)
{ cElements = abs(iHi - iLo) + 1; lLbound = iLo; }
Dim(const long c)
{ cElements = c; lLbound = 0; }
const Dim & operator=(const Dim & dim)
{ cElements = dim.cElements; lLbound = dim.lLbound; return *this; }
const Dim & operator=(const long c)
{ cElements = c; lLbound = 0; return *this; }
~Dim() {}
long Elements() { return cElements; }
long LBound() { return lLbound; }
long UBound() { return lLbound + cElements - 1; }
};
Notice first that the Dim object is inherited publicly from SAFEARRAYBOUND. This means that the Dim is a SAFEARRAYBOUND and you can use its data members—lLbound and cElements. These have to be public so that you can pass a Dim to system functions such as SafeArrayCreate. You can use a Dim the hard way:
Dim dim;
dim.lLBound = 4;
dim.cElements = 9;
but why bother, when you can do the following:
Dim dim(4, 12);
That's more like Visual Basic. But you can also just specify the number of elements and assume zero as the starting point:
Dim dim2(8); // Same as Dim dim2(0, 7)
Normally, you don't need a separate variable for Dim. Just create a temporary one in the assignment statement:
ArrayString as = Dim(1, 8); // Dim as(1 To 8) As String
So why didn't I just skip the Dim class and give arrays a constructor taking two arguments? Then you could define the array like this:
ArrayString as(1, 8); // Dim as(1 To 8) As String
True, this would have been easy, and you should feel free to add it if you want. But what happens with multidimensional arrays? Which makes more sense? This:
ArrayString2 as(Dim(1, 8), Dim(3, 9)); // Dim as(1 To 8, 3 To 9) As String
Or this:
ArrayString2 as(1, 8, 3, 9); // Dim as(1 To 8, 3 To 9) As String
It's arguable, but I find the separate Dim class easier to read. It's also more efficient internally.
Notice that I show a separate class, ArrayString2, for two-dimensional arrays. I tried to write a single SafeArray class that could handle multiple dimensions, but function overloading issues made it hard to implement cleanly. In this article, we'll only be using one-dimensional arrays, but you should have no problem cloning the SafeArray class as SafeArray2, SafeArray3, and so on. You won't be able to redimension a one-dimensional class to a two-dimensional class at run time.
The TestSafeArray function seems simple enough. It takes two parameters. One is an input array that the function will modify; the other is an empty output array that the function will create. The function would like to return another output array through the return value, but Visual Basic doesn't support direct return of arrays. They can only be returned in Variants (check out Visual Basic's GetAllSettings function), so that's what we'll do.
The function prototype looks simple enough:
Variant DLLAPI TestSafeArray(ArrayInteger & aiInOut, ArrayString & asOut);
Of course what you're really doing behind the typedefs is using templates:
Variant DLLAPI TestSafeArray(SafeArray<short, VT_I2> & aiInOut,
SafeArray<BSTR, VT_BSTR> & asOut);
SafeArray parameters must always be passed as pointers, but we use references to make the array objects look normal. You'll see how clean this looks in the implementation shortly.
Unfortunately, MKTYPLIB doesn't know anything about references, much less about templates. Here's how you're supposed to define the SAFEARRAY structures in a type library:
Variant WINAPI TestSafeArray([in, out] SAFEARRAY(short) * ai,
[out] SAFEARRAY(BSTR) * as);
You can see what's going on, but it looks almost as ugly as the template prototype. We can do better:
Variant WINAPI TestSafeArray([in, out] ArrayInteger ai,
[out] ArrayString as);
This simple definition is made possible by #define statements in the OLETYPE.ODL standard include file. For example:
#define ArrayLong SAFEARRAY(long) *
OLETYPE.ODL should be included in any type libraries that require SafeArrays. It includes WINTYPE.ODL, so you don't need to include both. You should put this file in a standard location known to your compiler (such as \MSDEV\INCLUDE for Microsoft Visual C++).
What would you like to do with an array? Well, if you had an input array you might want to examine elements. If the array were an in/out array, you might want to modify the contents. Here's one way of doing that:
iMid = aiInOut.LBound() + (aiInOut.Elements() / 2);
// Get middle value of array.
iVal = aiInOut.Get(iMid);
// Double it
iVal *= 2;
// Put modified version back.
aiInOut.Set(iVal, iMid);
There's nothing really wrong with this, and it may be the most efficient way to access a single array element. But how often do you access a single element? Every time you call the Get or Set method, the class in turn calls the SafeArrayGet or SafeArrayPut API function, which in turn calls the SafeArrayLock or SafeArrayUnlock API function. You don't need the extra function calls or the hassle.
Here's how you modify the entire array in a loop:
// Square each value, C++ style.
aiInOut.Lock();
for (i = 0; i < aiInOut.Elements(); i++) {
aiInOut[i] *= aiInOut[i];
}
aiInOut.Unlock();
You have to lock the array before accessing it, and unlock it when you're done. There's something strange here. This looks like a C++ array, starting at 0 and continuing as long as the index is less than the total number of elements. But what about the lower and upper boundaries the SafeArray is supposed to provide? Shouldn't you be looping from LBound() to UBound()? Well, it's a matter of taste. You can indeed do it that way if you prefer:
// Divide each by two, Visual Basic style.
aiInOut.Lock();
for (i = aiInOut.LBound(); i <= aiInOut.UBound(); i++) {
aiInOut(i) /= 2;
}
aiInOut.Unlock();
Notice that in this case you index with parentheses instead of brackets in the Visual Basic style. It makes sense, but how can C++ have Visual Basic indexing? Through the magic of operator overloading. The SafeArray class overloads the subscript operator (square brackets) for zero-based indexing. It overloads the function operator (parentheses) for boundary-based indexing. It may not be what C++ designer Bjarne Stroustrup had in mind, but it works.
You can copy an array in two ways—through initialization:
// Copy an array.
ArrayInteger aiCopy = aiInOut;
Or through assignment:
aiCopy = aiInOut;
Either way you end up with a completely new array containing the same data. It's a deep copy—if the arrays contain BSTRs, each will have its own separate but identical string for each element. SafeArray doesn't support shallow copies in which two array pointers point to the same array data. SafeArray does allow you to redimension arrays—provided that they're sizable:
// Redimension to throw away last element.
if (aiInOut.IsSizable()) {
aiInOut.ReDim(Dim(aiInOut.LBound(), aiInOut.UBound() - 1));
}
For this to work on an array passed from Visual Basic, the array must be created with ReDim rather than Dim.
' Use SafeArrayRedim on this one.
Dim aiModify() As Integer
Redim Preserve aiModify(1 To 8) As Integer
Often you'll want to create an array in a function and return it to Visual Basic through an out parameter. Here's an example:
// Create array of strings.
ArrayString as = Dim(4, 9);
String s = _W("Fan");
for (i = as.LBound(); i <= as.UBound(); i++) {
s[0] = L'F' + (WCHAR)i;
as(i) = s;
}
// Return it through out parameter.
asOut = as;
And here's how to return through a Variant return value:
// Create array of doubles.
ArrayDouble adbl = Dim(-5, 5);
for (i = adbl.LBound(); i <= adbl.UBound(); i++) {
adbl(i) = i * 3.1416;
}
// Return through Variant return value.
Variant vRet = (Variant)adbl;
return vRet;
SafeArray has a Variant type conversion operator that makes the assignment to the Variant return value possible.
Visual Basic supports a passing a varying number of arguments to a procedure. To the caller of the function, it may look like you're calling a list of unrelated arguments, but the Visual Basic implementer knows that there is actually only one parameter—the last one—called a ParamArray. The C++ implementer knows that this final parameter is actually a variable-length array of Variants in which some elements may be missing.
The Visual Basic parser reads the caller's argument list and creates a Variant array of the same size. It fills the array with the arguments, setting any missing arguments to an error value (DISP_E_PARAMNOTFOUND).
The calls might looks like this on the Basic side:
Debug.Print AddEmUp(7, 9.4, "4") ' 40.4
Debug.Print AddEmUp(, 9.4, , "24") ' 33.4
Debug.Print AddEmUp(7, "4") ' 31
Wait a minute! How can you add strings to numbers? That's what Variants are for. The C++ side of the code looks like this:
double DLLAPI AddEmUp(ParamArray & avParams)
{
try {
double dblRet = 0;
// Loop through the array, retrieving parameters.
avParams.Lock();
for (long i = 0; i < avParams.Elements(); i++) {
// Ignore missing ones.
if (!avParams[i].IsMissing()) {
dblRet += (double)avParams[i];
}
}
avParams.Unlock();
return dblRet;
} catch(Long e) {
ErrorHandler(e);
return 0.0;
}
}
The ParamArray type is actually another typedef, which happens to be exactly the same as ArrayVariant. There's a matching ODL define in OLETYPE.ODL. You have to specify the vararg attribute in the type library file so that Visual Basic will know that it should treat the last parameter as a ParamArray:
[
entry("AddEmUp"),
helpstring("Tests ParamArrays"),
vararg
]
double WINAPI AddEmUp([in] ParamArray params);
The event handler for the ParamArray button in the Cpp4VB sample program tests the AddEmUp function.
As with the String and Variant classes, you don't have to understand how SafeArray is implemented to use it. But I strongly recommend taking a look at the SafeArray implementation because it's not just another class. Implementing a class as a C++ template requires a whole different mind set. Things you always thought were cast in concrete turn out to be sinking in wet cement.
The first thing you need to discard is the quaint notion that you should put your implementation in a .Cpp file and your declarations in an .H file. There—I've already saved some of you many hours of wasted effort. I wish someone had told me that right up front.
What you need to know about template classes is that when you define one, you are actually defining as many classes as you can think up to replace the template placeholder. If the compiler treated this as a traditional C++ module, your hard disk would be filled with object files on every interminable compile. So the compiler only compiles the template code that you need.
For example, if your program uses the SafeArray template class for arrays of Variant, Long, and Single, those classes will be compiled as part of the module that uses them, but the Double, Currency, and String versions won't be compiled. So there really isn't such thing as a separate SafeArray object module, just a template definition. The module using the template needs to see both declarations and implementation, and therefore most C++ compilers require that everything must go in the .H file. There isn't any .Cpp file. Some people have worked around this by putting the implementation in a .Cpp file and then including it at the end of the .H file.
It's probably better, though, to change old habits than to fit old techniques inot new idioms. Templates require a new way of thinking. For example, you might have a bug that only shows up in certain instantiations of a template. A function overload that works fine with an array of Doubles might fail wretchedly with an array of Longs. Consider this:
void Do(Long i, T & t);
void Do(T & t, Long i);
When you create an array of Doubles, you get this:
void Do(Long i, Double & t);
void Do(Double & t, long i);
No problem. The compiler knows the different between Do(5.6, 5L) and Do(5L, 5.6). But if you create an array of Longs you get:
void Do(Long i, Long & t);
void Do(Long & t, Long i);
These are the same function and you'll get a compile-time error—but not until you try instantiating the Long version of the class. This is why (in short) I'm recommending that you create separate SafeArray2 and SafeArray3 classes for 2-D and 3-D arrays. If you think you can enhance the SafeArray class to handle multidimensional arrays without hitting this problem, be my guest.
I'm not going to go very deep into the implementation of the SafeArray class, but I will show you a few bits and pieces to give you the feel. Let's start with the class itself:
template<class T, VARTYPE vt>
class SafeArray
{
public:
// Constructors
SafeArray();
SafeArray(SAFEARRAY * psaSrc);
SafeArray(Dim & dim);
// Copy constructor
SafeArray(const SafeArray & saSrc);
// Destructor
~SafeArray();
// Operator equal
const SafeArray & operator=(const SafeArray & saSrc);
// Indexing
T & Get(long i);
T & Set(T & t, long i);
T & operator[](const long i); // C++ style (0-indexed)
T & operator()(const long i); // Basic style (LBound-indexed)
.
.
.
private:
SAFEARRAY * psa;
void Destroy();
Boolean IsConstructed();
void Constructed(Boolean f);
};
It looks just the same as any other class except for the template<class T, VARTYPE vt> at the start. This says that T will represent whatever type the array will be instantiated to contain, and vt will represent the Variant type constant. (The vt parameter is actually needed in only one place—calls to SafeArrayCreate in constructors—but in that one case there's no alternative.) You can see how the T is used in the declarations of Get, Set, and the subscript and function operators.
The SafeArray class has only one data member—psa, which is a pointer to a SAFEARRAY structure. When you pass SAFEARRAY structures directly, you pass a pointer to a pointer to a SAFEARRAY.
Variant DLLAPI TestSA(SAFEARRAY ** ppsaiInOut, SAFEARRAY ** ppsasOut);
When you use SafeArrays, you pass a reference (or pointer) to a SafeArray, which contains a pointer to a SAFEARRAY.
Variant DLLAPI TestSafeArray(ArrayInteger & aiInOut, ArrayString & asOut);
It comes to the same thing.
Implementations of SafeArray methods aren't nearly as clean as their declarations. You can probably guess the implementation of constructors (which simply call SafeArrayCreate), so let's skip to something more interesting:
template<class T, VARTYPE vt>
inline T & SafeArray<T,vt>::Set(T & t, long i)
{
HRESULT hres = SafeArrayPutElement(psa, &i, (T *)&t);
if (hres) throw hres;
return t;
}
template<class T, VARTYPE vt>
inline T & SafeArray<T,vt>::operator[](const long i)
{
if (i < 0 || i > Elements() - 1) throw DISP_E_BADINDEX;
return ((T*)psa->pvData)[i];
}
template<class T, VARTYPE vt>
T & SafeArray<T,vt>::operator()(const long i)
{
if (i < LBound() || i > UBound()) throw DISP_E_BADINDEX;
return ((T*)psa->pvData)[i - LBound()];
}
There's a whole lot of angle brackets floating around here, and there's no way to avoid them.
The Set function illustrates how system functions are called from template members. They almost always require the psa data member. Parameters of T type should normally be reference parameters so that if they turn out to be large data structures, they'll be passed by reference instead of on the stack.. Notice the use of the T type (whatever it may be) to cast the third parameter (a void pointer) of SafeArrayPutElement to a usable type.
In operator functions for indexing, the indexed value is returned by reference, so that you can either assign to it or read from it. So the statement:
ai[2] = iVal;
actually means:
*(ai.operator[](2)) = iVal;
The assignment actually takes place after the function call. That's why it wouldn't help to put lock and unlock statements in the operator function even if you wanted to.
From here I could go on to one additional article about how to use objects passed by Visual Basic to your C++ DLLs. Maybe I'll get around to writing that article someday.
But the real next step is to write your own OLE objects for Visual Basic. Objects can have many additional features, such as collections, Basic-style error handling, property pages, hidden or visible windows, and classification IDs. The problem is, how do you build objects?
Until a few months ago, you didn't have much choice. You could use MFC to create objects that were sometimes a little slow and heavy. Alternately, you could slog through acres of OLE documentation, figuring out how to write objects in raw C++ or, if you were really a wizard (some might say a masochist), in plain C. Now you have several other options, one of which is described on the CD published with my book Hardcore Visual Basic. Other alternatives are either available now or will be soon, using C++, Delphi®, Visual Basic, and perhaps even Java™. But regardless of the tools you choose to create your objects, you'll be working with the same standard OLE types described in this series.