previous page
next page

The CComSafeArray Smart SAFEARRAY Class

A Review of the COM SAFEARRAY Data Type

IDL provides several attributes for specifying arrays in COM interfaces. Attributes such as [size_is] and [length_is] enable you to adorn method definitions with information required to marshal these arrays across COM boundaries. Yet not all languages support arrays in the same way. For instance, some languages support zero-based arrays, while others require one-based arrays. Still others, such as Visual Basic, allow the application itself to decide whether the arrays it references are zero-based or one-based. Array storage varies from language to language as well: Some languages store elements in row-major order, and others use column-major order. To make the situation even worse, type libraries don't support the IDL attributes needed to marshal C-style IDL arrays; the MIDL compiler silently drops these attributes when generating type libraries.

To address the challenges of passing arrays between COM clients in a language-agnostic manner, Automation defines the SAFEARRAY data type. In much the same way as VARIANTs are self-describing generic data types, SAFEARRAYs are self-describing generic arrays. SAFEARRAYs are declared in IDL as follows:

interface IMyInterface : IUnknown {       
    HRESULT GetArray([out,retval]         
        SAFEARRAY(VARIANT_BOOL)* myArray);
};                                        

The VARIANT_BOOL parameter to the SAFEARRAY declaration indicates the data type of the elements in the SAFEARRAY. This type must be an Automation-compatible type as well, meaning that it must be one of the data types that can be contained in a VARIANT. The MIDL compiler preserves this information in the type library so that clients can discover the underlying type of the SAFEARRAY.

The C++ binding for the SAFEARRAY type is actually a struct that represents a self-describing array. It contains a description of the contents of the array, including the upper and lower bounds and the total number of elements in the array. The SAFEARRAY struct is defined in oaidl.h as follows:

typedef struct tagSAFEARRAY {     
    USHORT cDims;                 
    USHORT fFeatures;             
    ULONG cbElements;             
    ULONG cLocks;                 
    PVOID pvData;                 
    SAFEARRAYBOUND rgsabound[ 1 ];
} SAFEARRAY;                      

The upper and lower bounds for the SAFEARRAY are stored in the rgsabound array. Each element in this array is a SAFEARRAYBOUND structure.

typedef struct tagSAFEARRAYBOUND {
    ULONG cElements;              
    LONG lLbound;                 
} SAFEARRAYBOUND;                 

The leftmost dimension of the array is contained in rgsabound[0], and the rightmost dimension is in rgsabound[cDims - 1]. For example, an array declared with C-style syntax to have dimensions of [3][4] would have two elements in the rgsabound array. The first element at offset zero would have a cElements value of 3 and an lLbound value of 0; the second element at offset one would have a cElements value of 4 and also an lLbound value of 0.

The pvData field of the SAFEARRAY struct points to the actual data in the array. The cbElements array indicates the size of each element. As you can see, this data type is flexible enough to represent an array with an arbitrary number of elements and dimensions.

COM provides a number of APIs for managing SAFEARRAYs. These functions enable you to create, access, and destroy SAFEARRAYs of various dimensions and sizes. The following code demonstrates how to use these functions to manipulate two-dimensional SAFEARRAYs of double. The first step is to create an array of SAFEARRAYBOUND structures to indicate the number and size of the array dimensions:

SAFEARRAYBOUND rgsabound[2];
rgsabound[0].cElements = 3;
rgsabound[0].lLbound = 0;
rgsabound[1].cElements = 4;
rgsabound[1].lLbound = 0;

This code specifies a two-dimensional array with three elements in the first dimension (three rows) and four elements in the second dimension (four columns). This array is then passed to the SafeArrayCreate function to allocate the appropriate amount of storage:

SAFEARRAY* psa = ::SafeArrayCreate(VT_R8, 2, rgsabound);

The first parameter to this function indicates the data type for the elements of the array. The second parameter specifies the number of elements in the rgsabound array (for example, the number of dimensions). The final parameter is the array of SAFEARRAYBOUND structures describing each dimension of the SAFEARRAY. You can retrieve elements of the SAFEARRAY using the SafeArrayGetElement function, like this:

long rgIndices[] = { 2, 1 };
double lElem;
::SafeArrayGetElement(psa, rgIndices, (void*)&lElem);

This code retrieves the element stored at location [1][2]that is, the second row, third column. Confusingly, the rgIndices specifies the SAFEARRAY indices in reverse order: The first element of the rgIndices array specifies the rightmost dimension of the SAFEARRAY. You must manually free the SAFEARRAY and the data it contains using the SafeArrayDestroy function.

::SafeArrayDestroy(psa);

As you can see, manipulating SAFEARRAYs with these APIs is a bit tedious. Fortunately, ATL provides some relief in the form of a templatized wrapper class called CComSafeArray. This class is defined in atlsafe.h as follows:

template <typename T,                               
    VARTYPE _vartype = _ATL_AutomationType<T>::type>
class CComSafeArray {                               
  ...                                               
public:                                             
     LPSAFEARRAY m_psa;                             
}                                                   

This template class encapsulates a pointer to a SAFEARRAY as its only state. The first template parameter is the C++ type that will be stored in the internal SAFEARRAY. Recall that SAFEARRAYs can hold only Automation-compatible types as elementsthat is, data types that can be stored in a VARIANT. So, the second template parameter to CComSafeArray indicates the VARTYPE of the elements to be stored. Only a subset of the VARIANT-compatible types are supported with CComSafeArray. These are listed in Table 3.1.

Table 3.1. ARTYPEs CComSafeArray Supports

VARTYPE

C++ Type

VT_I1

char

VT_I2

short

VT_I4

int

VT_I4

long

VT_I8

longlong

VT_UI1

byte

VT_UI2

ushort

VT_UI4

uint

VT_UI4

ulong

VT_UI8

ulonglong

VT_R4

float

VT_R8

double

VT_DECIMAL

decimal

VT_VARIANT

variant

VT_CY

currency

VT_BSTR

BSTR

VT_DISPATCH

IDispatch pointer

VT_UNKNOWN

IUnknown pointer


The documentation indicates that the last three VARTYPEsBSTR, IDispatch, and IUnknown pointersare not supported. The documentation is wrong; CComSafeArray uses template specialization to accommodate the unique semantics of these data types. More on this comes later in this section.

The default value for the second template parameter employs a clever combination of templates and macros to automatically associate the C++ data type with the VARTYPE that the SAFEARRAY API functions must use internally. The default parameter value uses the _ATL_AutomationType dummy template, defined as follows:

template <typename T>          
struct _ATL_AutomationType { };

The DEFINE_AUTOMATION_TYPE_FUNCTION macro generates type mappings from the C++ data type to the appropriate VARTYPE. The type enum member holds the VARTYPE that CComSafeArray ultimately will use:

#define DEFINE_AUTOMATION_TYPE_FUNCTION(ctype,
 typewrapper, oleautomationtype) \            
    template <> \                             
    struct _ATL_AutomationType<ctype> { \     
    typedef typewrapper _typewrapper;\        
    enum { type = oleautomationtype }; \      
    static void* GetT(const T& t) { \         
        return (void*)&t; \                   
    } \                                       
};                                            

A series of these macros are declared in atlsafe.h to map CComSafeArray-supported types to the appropriate VARTYPE. Note that these macros include as the second macro parameter a typewrapper. This is interesting only for the four supported data types that require special handling: VARIANT, BSTR, IDispatch*, and IUnknown*.

DEFINE_AUTOMATION_TYPE_FUNCTION(CHAR, CHAR, VT_I1)               
DEFINE_AUTOMATION_TYPE_FUNCTION(SHORT, SHORT, VT_I2)             
DEFINE_AUTOMATION_TYPE_FUNCTION(INT, INT, VT_I4)                 
DEFINE_AUTOMATION_TYPE_FUNCTION(LONG, LONG, VT_I4)               
DEFINE_AUTOMATION_TYPE_FUNCTION(LONGLONG, LONGLONG, VT_I8)       
DEFINE_AUTOMATION_TYPE_FUNCTION(BYTE, BYTE, VT_UI1)              
DEFINE_AUTOMATION_TYPE_FUNCTION(USHORT, USHORT, VT_UI2)          
DEFINE_AUTOMATION_TYPE_FUNCTION(UINT, UINT, VT_UI4               
DEFINE_AUTOMATION_TYPE_FUNCTION(ULONG, ULONG, VT_UI4)            
DEFINE_AUTOMATION_TYPE_FUNCTION(ULONGLONG, ULONGLONG, VT_UI8)    
DEFINE_AUTOMATION_TYPE_FUNCTION(FLOAT, FLOAT, VT_R4)             
DEFINE_AUTOMATION_TYPE_FUNCTION(DOUBLE, DOUBLE, VT_R8)           
DEFINE_AUTOMATION_TYPE_FUNCTION(DECIMAL, DECIMAL, VT_DECIMAL)    
DEFINE_AUTOMATION_TYPE_FUNCTION(VARIANT, CComVariant, VT_VARIANT)
DEFINE_AUTOMATION_TYPE_FUNCTION(CY, CY, VT_CY)                   

With these definitions in hand, declaring an instance of CComSafeArray<long> would generate a second parameter of _ATL_Automation_Type<long>::type, where the exposed type member is equal to VT_I4.

Constructors and Destructor

The template parameter you pass to CComSafeArray establishes only the data type of the SAFEARRAY elements, not the number of dimensions or the size of each dimension. This information is established through one of the seven CComSafeArray constructors. The first constructor is the default (parameterless) constructor and simply initializes m_psa to NULL. Three other constructors create a new CComSafeArray instance from dimension and size information. The first of these constructors creates a one-dimensional array with ulCount elements and is indexed starting with lLBound:

explicit CComSafeArray(ULONG ulCount, LONG lLBound = 0);

Internally, this constructor uses these arguments to create an instance of a class that serves as a thin wrapper for the SAFEARRAYBOUND structure discussed earlier. The CComSafeArrayBound class exposes simple methods for manipulating the number of elements in a particular CComSafeArray dimension, as well as the starting index (lower bound) for that dimension. Note that this class derives directly from the SAFEARRAYBOUND structure, so it can be passed to methods that expect either a CComSafeArrayBound class or a SAFEARRAYBOUND structure.

class CComSafeArrayBound : public SAFEARRAYBOUND {             
    CComSafeArrayBound(ULONG ulCount = 0, LONG lLowerBound = 0)
    { ... }                                                    
    CComSafeArrayBound&                                        
    operator=(const CComSafeArrayBound& bound)                 
    { ... }                                                    
    CComSafeArrayBound& operator=(ULONG ulCount) { ... }       
    ULONG GetCount() const  { ... }                            
    ULONG SetCount(ULONG ulCount)  { ... }                     
    LONG GetLowerBound() const  { ... }                        
    LONG SetLowerBound(LONG lLowerBound)  { ... }              
    LONG GetUpperBound() const  { ... }                        
};                                                             

A quick look at the implementation for the CComSafeArray(ULONG, LONG) constructor demonstrates how all the nondefault constructors use the CComSafeArrayBound wrapper class:

explicit CComSafeArray(ULONG ulCount, LONG lLBound = 0)
    : m_psa(NULL) {                                    
    CComSafeArrayBound bound(ulCount, lLBound);        
    HRESULT hRes = Create(&bound);                     
    if (FAILED(hRes))                                  
       AtlThrow(hRes);                                 
}                                                      

An instance of CComSafeArrayBound is created and passed to the Create member function, which is itself a thin wrapper over the SAFEARRAY API functions. As shown in the following code fragment, Create uses the SafeArrayCreate API to support building a SAFEARRAY with any number of dimensions:

HRESULT Create(const SAFEARRAYBOUND *pBound, UINT uDims = 1) {
    ATLASSERT(m_psa == NULL);                                 
    ATLASSERT(uDims > 0);                                     
    HRESULT hRes = S_OK;                                      
    m_psa = SafeArrayCreate(_vartype, uDims,                  
    const_cast<LPSAFEARRAYBOUND>(pBound));                    
    if (NULL == m_psa)                                        
        hRes = E_OUTOFMEMORY;                                 
    else                                                      
        hRes = Lock();                                        
    return hRes;                                              
}                                                             

This first constructor just shown is probably the most frequently used. One-dimensional SAFEARRAYs are much more common than multidimensional SAFEARRAYs, and C++ developers are accustomed to zero-based array indexing. You make use of this simple constructor with code such as the following:

// create a 1-D zero-based SAFEARRAY of long with 10 elements
CComSafeArray<long> sa(10);

// create a 1-D one-based SAFEARRAY of double with 5 elements
CComSafeArray<double> sa(5,1);

The second CComSafeArray constructor enables you to pass in a SAFEARRAYBOUND structure or a CComSafeArrayBound instance:

explicit CComSafeArray(const SAFEARRAYBOUND& bound);

This constructor is invoked when you write code similar to the following:

CComSafeArrayBound bound(5,1);    // 1-D one-based array
CComSafeArray<long> sa(bound);

This constructor is arguably less useful and less succinct than passing the bounds information directly via the first constructor shown. You use the third constructor to create a multidimensional SAFEARRAY. This constructor accepts an array of SAFEARRAYBOUND structures or CSafeArrayBound instances, along with a UINT parameter to indicate the number of dimensions:

explicit CComSafeArray(const SAFEARRAYBOUND *pBound, UINT uDims = 1);

You create a multidimensional CComSafeArray with this constructor as follows:

// 3-D array with all dimensions
// left-most dimension has 3 elements
CComSafeArrayBound bound1(3);
// middle dimension has 4 elements
CComSafeArrayBound bound2(4);
// right-most dimension has 5 elements
CComSafeArrayBound bound3(5);

// equivalent C-style array indices would be [3][4][5]
CComSafeArrayBound rgBounds[] = { bound1, bound2, bound3 };
CComSafeArray<int> sa(rgBounds, 3);

Note that nothing prevents you from creating different starting indices for the different dimensions of the SAFEARRAYnothing but your conscience that is. This would be extraordinarily confusing for any code that uses this type. In any event, as mentioned previously, multidimensional SAFEARRAYs are pretty rare creatures in reality, so we won't belabor the point.

The remaining three CComSafeArray constructors create an instance from an existing SAFEARRAY or CComSafeArray. They are declared as follows:

CComSafeArray(const SAFEARRAY *psaSrc) : m_psa(NULL);   
CComSafeArray(const SAFEARRAY& saSrc) : m_psa(NULL);    
CComSafeArray(const CComSafeArray& saSrc) : m_psa(NULL);

All three constructors do the same thing: check for a NULL source and delegate to the CopyFrom method to duplicate the contents of the source instance. CopyFrom accepts a SAFEARRAY* and CComSafeArray provides a SAFEARRAY* cast operator, so the third constructor delegates to the CopyFrom method as well. This produces a clone of the source array. The following code demonstrates how it instantiates a CComSafeArray from an existing instance:

CComSafeArray<int> saSrc(5);    // source is 1-D array of 5 ints
// allocate storage for 1-D array of 5 ints
// and copy contents of source
CComSafeArray<int> saDest(saSrc);

The destructor for CComSafeArray is quite simple as well. It automatically releases the resources allocated for the SAFEARRAY when the instance goes out of scope. The implementation simply delegates to the Destroy method, which is defined as follows:

HRESULT Destroy() {                            
    HRESULT hRes = S_OK;                       
    if (m_psa != NULL) {                       
        hRes = Unlock();                       
        if (SUCCEEDED(hRes)) {                 
            hRes = SafeArrayDestroy(m_psa);    
            if (SUCCEEDED(hRes))               
                m_psa = NULL;                  
        }                                      
    }                                          
    return hRes;                               
}                                              

The Destroy method first calls Unlock to decrement the lock count on the internal SAFEARRAY and then simply delegates to the SafeArrayDestroy method. The significance of lock counting SAFEARRAYs is discussed shortly.

Assignment

CComSafeArray defines two assignment operators. Both duplicate the contents of the right-side instance, clearing the contents of the left-side instance beforehand. These operators are defined as follows:

CComSafeArray<T>& operator=(const CComSafeArray& saSrc) {
    *this = saSrc.m_psa;                                 
    return *this;                                        
}                                                        
CComSafeArray<T>& operator=(const SAFEARRAY *psaSrc) {   
    ATLASSERT(psaSrc != NULL);                           
HRESULT hRes = CopyFrom(psaSrc);                         
    if (FAILED(hRes))                                    
        AtlThrow(hRes);                                  
    return *this;                                        
}                                                        

The assignment statement in the first line of the first operator delegates immediately to the second operator that accepts a SAFEARRAY* parameter. CopyFrom clears the contents of the destination SAFEARRAY by eventually calling SafeArrayDestroy to free resources allocated when the target SAFEARRAY was created. This code gets invoked with code such as the following:

CComSafeArray<long> sa1(10);
// do something interesting with sa1
CComSafeArray<long> sa2(5);

// free contents of sa1, duplicate contents
// of sa2 and put into sa1
sa1 = sa2;

The Detach and Attach Methods

As with the CComVariant and CComBSTR classes discussed earlier, the CComSafeArray class wraps a data type that must be carefully managed if resource leaks are to be avoided. The storage allocated for the encapsulated SAFEARRAY must be explicitly created and freed using the SAFEARRAY API functions. In fact, two chunks of memory must be managed: the SAFEARRAY structure itself and the actual data contained in the SAFEARRAY.

Just as with CComVariant and CComBSTR, the CComSafeArray class provides Attach and Detach methods to wrap a preallocated SAFEARRAY:

HRESULT Attach(const SAFEARRAY *psaSrc) {              
    ATLENSURE_THROW(psaSrc != NULL, E_INVALIDARG);     
                                                       
    VARTYPE vt;                                        
    HRESULT hRes = ::ATL::AtlSafeArrayGetActualVartype(
        const_cast<LPSAFEARRAY>(psaSrc), &vt);         
    ATLENSURE_SUCCEEDED(hRes);                         
    ATLENSURE_THROW(vt == GetType(), E_INVALIDARG);    
    hRes = Destroy();                                  
    m_psa = const_cast<LPSAFEARRAY>(psaSrc);           
    hRes = Lock();                                     
    return hRes;                                       
}                                                      
                                                       
LPSAFEARRAY Detach() {                                 
    Unlock();                                          
    LPSAFEARRAY pTemp = m_psa;                         
    m_psa = NULL;                                      
    return pTemp;                                      
    }                                                  

The Attach operation first checks to see if the type contained in the SAFEARRAY being attached matches the type passed as a template parameter. If the type is correct, the method next releases its reference to the encapsulated SAFEARRAY by calling Destroy. We glossed over the Destroy method when we presented it previously, but you'll note that the first thing Destroy did was call the CComSafeArray's Unlock method. The lock count in the SAFEARRAY structure is an interesting historical leftover.

Back in the days of 16-bit Windows, the OS couldn't rely on having a virtual memory manager. Every chunk of memory was dealt with as a direct physical pointer. To fit into that wonderful world of 640KB, memory management required an extra level of indirection. The GlobalAlloc API function that's still with us is an example. When you allocate memory via GlobalAlloc, you don't get an actual pointer back. Instead, you get an HGLOBAL. To get the actual pointer, you call GlobalLock and pass it the HGLOBAL. When you're done working with the pointer, you call GlobalUnlock. This doesn't actually free the memory; if you call GlobalLock again on the same HGLOBAL, your data will still be there, but on 16-bit Windows the pointer you got back could be different. While the block is unlocked, the OS is free to change the physical address where the block lives by copying the contents.

Today, of course, the virtual memory managers inside modern CPUs handle all this. Still, some vestiges of those old days remain. The SAFEARRAY is one of those vestiges. You are not allowed to do this to access a SAFEARRAY's data:

SAFEARRAY *psa = ::SafeArrayCreateVector(VT_I4, 0, 10);
// BAD - this pointer may not be valid!
int *pData = reinterpret_cast<int *>(pda->pvData);
// BOOM (maybe)
pData[0] = 5;

Instead, you need to first lock the SAFEARRAY:

SAFEARRAY *psa = ::SafeArrayCreateVector(VT_I4, 0, 10);
// GOOD - this will allocate the actual storage for the data
::SafeArrayLock(psa);
// Now the pointer is valid
int *pData = ( int * )(pda->pvData);
pData[0] = 5;
// Unlock after we're done
::SafeArrayUnlock( psa );

Locking the SAFEARRAY actually allocates the storage for the data if it doesn't already exist and sets the pvData field of the SAFEARRAY structure. Several different APIs perform this function. You can't just do psa->cLocks++; you must call an appropriate API function.

In the bad old days, it was important that handles got unlocked as quickly as possible; if they didn't, the OS couldn't move memory around and eventually everything ground to a halt as memory fragmentation grew. These days, there's no need to worry about unlocking, but the API remains. So, the CComSafeArray takes a simple approach: It locks the data as soon as it gets the SAFEARRAY and doesn't unlock it until the SAFEARRAY is either Destroyed or Detached.

You usually use Attach inside a method implementation to wrap a SAFEARRAY that has been passed to you:

STDMETHODIMP SomeClass::AverageArray(/* [in] */ SAFEARRAY* psa,
    /* [out] */ LONG* plAvg) {
    if (!plAvg) return E_POINTER;
    CComSafeArray<long> sa; // Note: no type check is done
                            // against psa type
    sa.Attach(psa);         // we're pointing at the same
                            // memory as psa

    ... perform some calculations

    sa.Detach(); // Must detach here or risk a crash
    return S_OK;
}

When you want to return a SAFERRAY from a method call, turn to the Detach operation, as in the following example:

STDMETHODIMP SomeClass::get_Array(/* [out] */ SAFEARRAY** ppsa) {
    if (!ppsa) return E_POINTER;
    CComSafeArray<long> sa(10);

    ... populate sa instance

    // no resources released when we leave scope
    // and no copying performed
    *ppsa = sa.Detach();
    return S_OK;
}

Attach and Detach don't do any copying of the SAFEARRAY, and with the lock count in place, you might be tempted to think of the lock count as a kind of reference counting. Unfortunately, ATL 8 has a bug in the implementation of the Destroy method that makes this use of CComSafeArray problematic. Consider this code sample:

STDMETHODIMP SomeClass::DontDoThis(SAFEARRAY* psa) {
    // We have two references to the safearray
    CComSafeArray<long> sa1, sa2;
    sa1.Attach(psa);
    sa2.Attach(psa);

    // manipulate the array here
    // BUG: Don't do this
    sa2.Destroy( );
}

The explicit call to sa2.Destroy() will not actually destroy the underlying SAFEARRAY; this makes sense because there are still outstanding references (and locks) on the underlying data structure. It did, however, call Unlock(). Here's the bug: Even though Destroy was called, sa2 thinks that it's holding on to a valid reference to a SAFEARRAY. As a result, the destructor of sa2 calls Destroy() again, resulting in too many calls to Unlock(). The results afterward are potentially not pretty. To avoid this bug, when you're using CComSafeArray, never Attach multiple CComSafeArray objects to the same SAFEARRAY pointer.

CComSafeArray Operations

Several methods are provided for retrieving information about the size and shape of a CComSafeArray instance:

LONG GetLowerBound(UINT uDim = 0) const;
LONG GetUpperBound(UINT uDim = 0) const;
ULONG GetCount(UINT uDim = 0) const;    
UINT GetDimensions() const;             
VARTYPE GetType() const ;               
bool IsSizable() const;                 

All these methods are fairly simple and self-explanatory. GetLowerBound and GetUpperBound return the lower and upper bounds of a particular dimension of the SAFEARRAY. The GetCount method takes a specific dimension number and returns the number of elements in that dimension. GeTDimensions returns the total number of dimensions in the SAFEARRAY, also known as the array rank. You can query the Automation VARTYPE with the GetType method. IsSizable indicates whether the SAFEARRAY can be resized. Recall that the definition of the SAFEARRAY data type included an fFeatures bit field that stores information about how the array is allocated.

typedef struct tagSAFEARRAY {
    ...                      
    USHORT fFeatures;        
    ...                      
} SAFEARRAY;                 

The SAFEARRAY API functions use this information to properly release elements when the SAFEARRAY is destroyed. The FADF_FIXEDSIZE bit indicates whether the SAFEARRAY can be resized. By default, the SAFEARRAY created with an instance of CComSafeArray is resizable, so the IsSizable method returns trUE. CComSafeArray doesn't expose any methods for directly manipulating the fFeatures flags, so they would change from their default values only if you directly access the encapsulated SAFEARRAY, if the SAFEARRAY passed to the CComSafeArray constructor has different values, or if an Attach is performed on a SAFEARRAY that manipulated the fFeatures field. You can access the internal SAFEARRAY directly with the GetSafeArrayPtr method:

LPSAFEARRAY* GetSafeArrayPtr() {
    return &m_psa;              
}                               

If a CComSafeArray instance is resizable, clients can use two different Resize methods to grow or shrink a SAFEARRAY:

HRESULT Resize(ULONG ulCount, LONG lLBound = 0);
HRESULT Resize(const SAFEARRAYBOUND *pBound);   

The first version takes the new number of elements and lower bound, constructs a SAFEARRAYBOUND structure from the supplied parameters, and delegates the real work to the second version that accepts a SAFEARRAYBOUND* parameter. This second version of Resize first verifies that the SAFEARRAY is resizable and then relies upon the SAFEARRAY API SafeArrayRedim to do the heavy lifting. The first thing to note is that only the least-significant (rightmost) dimension of a SAFEARRAY can be resized. So, you can change the size of a SAFEARRAY with dimensions [3][5][7] to one with dimensions [3][5][4], but you cannot change it to have dimensions [6][5][7]. If the resizing operation reduces the size of the SAFEARRAY, SafeArrayRedim deallocates the elements beyond the new bounds. If the operation increases the size of the SAFEARRAY, SafeArrayRedim allocates and initializes the appropriate number of new elements.

Be warned, there's a nasty bug in the implementation of the Resize method:

HRESULT Resize(const SAFEARRAYBOUND *pBound)                              { 
    ATLASSUME(m_psa != NULL);                                               
    ATLASSERT(pBound != NULL);                                              
    if (!IsSizable()) {                                                     
        return E_FAIL;                                                      
    }                                                                       
    HRESULT hRes = Unlock();                                                
    if (SUCCEEDED(hRes)) {                                                  
        hRes = SafeArrayRedim(m_psa, const_cast<LPSAFEARRAYBOUND>(pBound)); 
        if (SUCCEEDED(hRes)) {                                              
            hRes = Lock();                                                  
        }                                                                   
    }                                                                       
    return hRes;                                                            
}                                                                           

If the underlying call to SafeArrayRedim fails, the call to relock the SAFEARRAY is never made. When that happens, everything falls apart, and the destructor might even fail. If you call Resize, be very careful to check the return HRESULT. If it's a failure, you really can't assume anything about the state of the encapsulated SAFEARRAY. The best bet is to Detach it and clean up manually.

Microsoft has agreed that this is a bug but was unable to fix it in time for the Visual Studio 2005 release. Hopefully, it'll be officially fixed soon.

CComSafeArray also provides three useful Add functions that you can use to append elements in a SAFEARRAY to the end of an existing CComSafeArray instance. Note that these methods work for only one-dimensional SAFEARRAYs. All three versions will assert if the Add method is invoked on a CComSafeArray instance that contains a multidimensional SAFEARRAY.

HRESULT Add(const T& t, BOOL bCopy = TRUE);                
HRESULT Add(ULONG ulCount, const T *pT, BOOL bCopy = TRUE);
HRESULT Add(const SAFEARRAY *psaSrc);                      

The first version of Add tacks on a single element to the end of the SAFEARRAY. If the SAFEARRAY is NULL, Add first invokes the Create method to allocate an empty SAFEARRY. Resize then increases the size of the SAFEARRAY by one, and the SetAt method is called to insert the value of the t parameter into the last element of the SAFEARRAY. The bCopy parameter is discussed further in a moment when we examine the CComSafeArray accessors: SetAt and GetAt. For now, simply understand that this parameter controls whether the CComSafeArray is appended with an independent duplicate of the new item or whether it actually takes ownership of the item. The second version of Add accepts a count and an array of items to append. It works similar to the single-element version: First, it creates an empty SAFEARRAY, if necessary; then, it calls Resize to grow the SAFEARRAY by ulCount elements. SetAt is invoked within a loop to initialize the new SAFEARRAY elements with the value of the items supplied in the pT parameter. Finally, the third version of Add accepts a pointer to a SAFEARRAY and appends all elements that it contains to the end of the CComSafeArray instance. This version of Add relies upon Resize and SetAt to do its work in exactly the same manner as do the other two versions. Here's how you might use these methods in your own code:

CComSafeArray<int> sa;  // sa::m_psa is NULL
sa.Add(7);              // sa allocated and now contains { 7 }

int rgVal[] = { 8, 9 };
sa.Add(2, rgVal);       // sa now contains { 7, 8, 9 }

sa.Add(sa);             // sa now contains { 7, 8, 9, 7, 8, 9 }
                        // see discussion of cast operators to
                        // understand what makes this line work

Warning: The various Add overloads call the Resize method under the hood, so they're subject to the same buggy behavior if Resize fails.

CComSafeArray Element Accessors

CComSafeArray provides five methods for reading and writing individual elements of the encapsulated SAFEARRAY. Three of these methods are used for accessing one-dimensional SAFEARRAYs. The GetAt method comes in two flavors.

const typename _ATL_AutomationType<T>::_typewrapper&      
GetAt(LONG lIndex) const {                                
    ATLASSUME(m_psa != NULL);                             
    if(m_psa == NULL)                                     
        AtlThrow(E_FAIL);                                 
    LONG lLBound = GetLowerBound();                       
    ATLASSERT(lIndex >= lLBound);                         
    ATLASSERT(lIndex <= GetUpperBound());                 
    if ((lIndex < lLBound) || (lIndex > GetUpperBound())) 
        AtlThrow(E_INVALIDARG);                           
                                                          
    return ( (_ATL_AutomationType<T>::_typewrapper*)      
        m_psa->pvData )[lIndex-lLBound];                  
}                                                         
                                                          
_ATL_AutomationType<T>::_typewrapper& GetAt(LONG lIndex) {
    // code identical to const version                    
}                                                         

The two GetAt methods differ only in that the first version uses the const qualifier to enforce read-only semantics for the accessed element. The methods retrieve the upper and lower bounds and validate the specified index against these bounds. Note that the lIndex passed in is the index relative to the lLBound defined for the CComSafeArray instance, which might or might not be zero. To retrieve the requested element, the pvData field of the encapsulated SAFEARRAY is cast to the element type; conventional C-style pointer arithmetic does the rest.

At this point, it's worth examining the significance of the _typewrapper field of the _ATL_AutomationType template class presented earlier. Recall that a series of DEFINE_AUTOMATION_TYPE_FUNCTION macros associated the supported C++ data types with both their corresponding Automation VARTYPE as well as a wrapper class. Only one DEFINE_AUTOMATION_TYPE_FUNCTION macro actually supplied a real wrapper class for the C++ data typeall the others shown so far simply use the actual C++ type as the wrapper type. The one macro mapped the VARIANT data type to the CComVariant wrapper class and to a VARTYPE value of VT_VARIANT. This internally sets _typewrapper to CComVariant and allows CComSafeArray to both leverage the convenient semantics of CComVariant in its internal implementation and return CComVariant elements of the SAFEARRAY to the client. Using a wrapper class in the typecasting code within GetAt relies upon the fact that CComVariant holds the encapsulated VARIANT as its only state, as discussed in the previous section on the CComVariant class. An instance of CComVariant resides at precisely the same memory address and occupies precisely the same storage as the encapsulated VARIANT. So, CComSafeArray can seamlessly deal in terms of the _typewrapper type internally and expose the wrapper type to the client as a convenience. Note that wrapper classes for element types must hold the encapsulated type as their only state if this scheme is to work correctly. You'll see in a moment that the GetAt method isn't the only method that breaks down if this isn't the case.

The list of DEFINE_AUTOMATION_TYPE_FUNCTION macros didn't generate type mappings support for two other important SAFEARRAY element types that CComSafeArray actually supports, even though the current ATL documentation doesn't mention them. Instead of macros, CComSafeArray provides support for elements of type BSTR, IDispatch*, and IUnknown* tHRough template specialization. The template specialization for all three data types looks very similar; we examine the one for BSTR as an example because you're already familiar with the associated wrapper class: CComBSTR. The wrapper class for IDispatch* and IUnknown* is CComPtr, presented in detail in the later section "The CComPtr and CComQIPtr Smart Pointer Classes." The specialization for BSTR fills the role of the DEFINE_AUTOMATION_TYPE_FUNCTION macro, in that it sets the _typewrapper member of _ATL_AutomationType to the CComBSTR wrapper class and sets the type member to VT_BSTR, as you can see in the following code:

template <>                           
struct _ATL_AutomationType<BSTR> {    
    typedef CComBSTR _typewrapper ;   
    enum { type = VT_BSTR};           
    static void* GetT(const BSTR& t) {
        return t;                     
    }                                 
};                                    

Similarly, the specialization for IDispatch* sets type to VT_DISPATCH and _typewrapper to CComPtr; the one for IUnknown* sets type to VT_UNKNOWN and _typewrapper to CComPtr.

You should recall that, like CComVariant, CComBSTR holds in its m_str member the encapsulated BSTR as its only state. Thus, the code shown previously in the GetAt method works fine for CComSafeArrays that contain BSTR elements. Also note that the specialization shown earlier defines the GetT method differently than the other supported data types. This method is only used by the element accessor functions for multidimensional SAFEARRAYs, in which a void* pointer to the destination buffer must be provided to the SafeArrayGetElement API. The GetT implementation that the DEFINE_AUTOMATION_TYPE_FUNCTION macro generates for all the other data types returns the address of the encapsulated data. In the case of a BSTR, the data type is already a pointer to the data, so GetT is specialized to return the encapsulated type itself instead of a pointer to the encapsulated type. The specializations for IDispatch* and IUnknown* implement GetT in precisely the same way as well because they are also inherently pointer types.

CComSafeArray provides the SetAt method for writing to specific elements. The method is defined as follows:

HRESULT SetAt(LONG lIndex, const T& t, BOOL bCopy = TRUE) {
    bCopy;                                                 
    ATLASSERT(m_psa != NULL);                              
    LONG lLBound = GetLowerBound();                        
    ATLASSERT(lIndex >= lLBound);                          
    ATLASSERT(lIndex <= GetUpperBound());                  
    ((T*)m_psa->pvData)[lIndex-lLBound] = t;               
    return S_OK;                                           
}                                                          

SetAt first ensures that the encapsulated SAFEARRAY is non-NULL and then validates the index passed in against the upper and lower bounds defined for the SAFEARRAY. The assignment statement copies the element data provided into the appropriate location in the encapsulated SAFEARRAY. Assigning a new value to a SAFEARRAY element is accomplished with code like the following:

CComSafeArray<long> sa(5);
long lNewVal = 14;
// replace the 4th element with the value 14
sa.SetAt(3, lNewVal);

The relevance of the bCopy parameter becomes evident only when you turn to the four specializations of the SetAt parameter that are provided for SAFEARRAYs: BSTR, VARIANT, IDispatch*, and IUnknown*. Each of these data types requires special handling when performing assignment, so CComSafeArray specializes SetAt to enforce the correct semantics. The specialization for BSTR first uses SysFreeString to clear the existing element in the array and then assigns it either to a copy of the provided BSTR or to the BSTR parameter itself, depending on the value of the bCopy parameter.

template<>                                               
HRESULT CComSafeArray<BSTR>::SetAt(LONG lIndex,          
    const BSTR& strData, BOOL bCopy) {                   
    // validation code omitted for clarity               
                                                         
    BSTR strOrg = ((BSTR*)m_psa->pvData)[lIndex-lLBound];
    if (strOrg)                                          
        ::SysFreeString(strOrg);                         
                                                         
    if (bCopy) {                                         
        BSTR strTemp = ::SysAllocString(strData);        
        if (NULL == strTemp)                             
            return E_OUTOFMEMORY;                        
        ((BSTR*)m_psa->pvData)[lIndex-lLBound] = strTemp;
    }                                                        
    else                                                 
        ((BSTR*)m_psa->pvData)[lIndex-lLBound] = strData;
    return S_OK;                                         
}                                                        

When bCopy is TRUE, the caller maintains ownership of the strData BSTR parameter, because CComSafeArray will be working with its own private copy. When bCopy is FALSE, CComSafeArray takes ownership of the strData BSTR; the caller must not attempt to free it, or errors will occur when this element is accessed from the SAFEARRAY. The following code snippet demonstrates this important difference:

BSTR bstr1 = ::SysAllocString(OLESTR("Go Longhorns!"));
BSTR bstr2 = ::SysAllocString(OLESTR("ATL Rocks!"));

CComSafeArray<BSTR> sa(5);
sa.SetAt(2, bstr1, true);  // sa generates its own copy of bstr1
sa.SetAt(3, bstr2, false); // sa assigns element to bstr2
::SysFreeString(bstr1);    // ok, sa still has a copy
::SysFreeString(bstr2);    // wrong!!! we don't own bstr2

VARIANT elements in SAFEARRAYs require special handling as well. The code for the specialized version of SetAt for VARIANTs is very similar to that shown earlier for BSTR. The main differences are that the original element is cleared using VariantClear and the copy is performed using VariantCopyInd if bCopy is TRUE. The code that implements SetAt for IDispatch* and IUnknown* type elements is identical (okay, the variable names are differentpDisp for IDispatch* and pUnk for IUnknown*). In either case, the original interface pointer element is Release'd before assignment and then is AddRef'd if bCopy is trUE. Again, this means that the caller is transferring ownership of the interface pointer to the CComSafeArray if bCopy is FALSE and should not then call Release on the pointer passed to the SetAt method. CComSafeArray ultimately generates a call to Release on each element in the SAFEARRAY when the instance is destroyed, so improper handling leads to a double release of the interface pointer and the all-too-familiar exception as a reward. Both proper and improper interface pointer element assignment are demonstrated in the following code:

IUnknown* pUnk1, pUnk2;
// assign both pointers to refer to an object
CComSafeArray<IUnknown*> sa(5);
sa.SetAt(2, pUnk1, true);  // sa calls AddRef on pUnk1
sa.SetAt(3, pUnk2, false); // sa assigns element to pUnk2
                           // without AddRefing
pUnk1->Release();          // ok, refcount non-zero because
                           // of sa AddRef
pUnk2->Release();          // wrong!!! we don't own pUnk2

The remaining two methods for accessing SAFEARRAY elements apply to multidimensional SAFEARRAYs. MultiDimGetAt and MultiDimSetAt provide read-and-write access to SAFEARRAY elements housed in a multidimensional SAFEARRAY. Both methods are very thin wrappers on top of the SafeArrayGetElement and SafeArrayPutElement API functions, respectively.

HRESULT MultiDimGetAt(const LONG *alIndex, T& t) {               
    ATLASSERT(m_psa != NULL);                                    
    return SafeArrayGetElement(m_psa,                            
        const_cast<LONG*>(alIndex), &t);                         
}                                                                
HRESULT MultiDimSetAt(const LONG *alIndex, const T& t) {         
    ATLASSERT(m_psa != NULL);                                    
    return SafeArrayPutElement(m_psa, const_cast<LONG*>(alIndex),
    _ATL_AutomationType<T>::GetT(t));                            
}                                                                

The alIndex parameter specifies an array of SAFEARRAY indices. The first element in the alIndex array is the index of the rightmost dimension; the last element is the index of the leftmost dimension. You make use of these functions like this:

// 2-D array with all dimensions
// left-most dimension has 3 elements
CComSafeArrayBound bound1(3);
// right-most dimension has 4 elements
CComSafeArrayBound bound2(4);

// equivalent C-style array indices would be [3][4]
CComSafeArrayBound rgBounds[] = { bound1, bound2 };
CComSafeArray<int> sa(rgBounds, 2);

int rgIndElement1[] = { 0, 1 };    // access element at sa[1][0]
int rgIndElement2[] = { 3, 2 };    // access element at sa[2][3]

long lVal = 0;
// retrieve value at sa[1][0]
sa.MultiDimGetAt(rgIndElement1, lVal);

// multiply value by 2 and store it
// in element located at sa[2][3]
sa.MultiDimSetAt(rgIndElement2, lVal*=2);

CComSafeArray Operators

CComSafeArray defines four operators that provide some syntactic convenience for accessing elements of a SAFEARRAY:

const typename                       
_ATL_AutomationType<T>::_typewrapper&
operator[](int nIndex) const {       
    return GetAt(nIndex);            
}                                    
typename                             
_ATL_AutomationType<T>::_typewrapper&
operator[](int nIndex) {             
    return GetAt(nIndex);            
}                                    
const typename                       
_ATL_AutomationType<T>::_typewrapper&
operator[](LONG nIndex)              
    const {                          
    return GetAt(nIndex);            
}                                    
typename                             
_ATL_AutomationType<T>::_typewrapper&
operator[](LONG nIndex) {            
    return GetAt(nIndex);            
}                                    

As you can see, all these operators simply delegate to the GetAt accessor method, discussed in the previous section. They differ only in the type of index and whether the const qualifier is specified. These operators enable you to write code with CComSafeArray that looks very much like the code you use to manipulate C-style array elements.

CComSafeArray<int> sa(5);
ATLASSERT(sa[2] == 0);
sa[2] = 17;
ATLASSERT(sa[2] == 17);

CComSafeArray also provides two cast operators. Both implementations are trivial, serving only to expose the encapsulated SAFEARRAY. Nevertheless, they provide some syntactic convenience in some situations, such as when you want to pass a CComSafeArray instance to a function that expects a SAFEARRAY*.

operator const SAFEARRAY *() const {
    return m_psa;                   
}                                   
operator LPSAFEARRAY() {            
    return m_psa;                   
}                                   

Unfortunately, CComSafeArray does not supply one operator: an overload of operator&. Without it, you can't use CComSafeArray as a wrapper for an out parameter like this:

HRESULT CreateANewSafeArray( SAFEARRAY** ppsa ) {
    *ppsa = SafeArrayCreateVector(VT_I4, 1, 15 );
    return S_OK;
}
HRESULT UseCreatedSafeArray( ) {
    CComSafeArray< int > sa;
    HRESULT hr = CreateANewSafeArray( &sa );
}

The previous code will not compile but will fail with this error:

error C2664: CreateANewSafeArray : cannot convert
parameter 1 from
    ATL::CComSafeArray<T> *__w64 to SAFEARRAY **
        with
        [
            T=int
        ]
        Types pointed to are unrelated;
        conversion requires reinterpret_cast,
        C-style cast or function-style cast

This use differs from most of the other ATL smart types, which let you do exactly this, but there's a good reason for the disparity. Imagine that the ATL team had included the overloaded operator&. What happens in this case?

HRESULT CreateANewSafeArray( SAFEARRAY** ppsa ) {
    *ppsa = SafeArrayCreateVector(VT_BSTR, 1, 15 );
    return S_OK;
}

HRESULT UseCreatedSafeArray( ) {
    CComSafeArray< int > sa;
    HRESULT hr = CreateANewSafeArray( &sa );
}

The C++ compiler can't tell what the returned SAFEARRAY actually contains; that information is available only at runtime. The compiler would have to allow the conversion, and we now have a CComSafeArray<int> wrapping a SAFEARRAY that actually contains BSTRs. Nothing good can come of this, so the operator& overload was left out. Instead, you can do this:

HRESULT UseCreatedSafeArray( ) {
    SAFEARRAY *psa = null;
    HRESULT hr = CreateANewSafeArray( &psa );
    CComSafeArray< int > sa;
    sa.Attach( psa );
}

The error will now be detected at runtime inside the CComSafeArray::Attach method.

The GetSafeArrayPtr() method, mentioned earlier, explicitly retrieves a pointer to the stored SAFEARRAY. It can be used like this:

HRESULT UseCreatedSafeArray( ) {
    CComSafeArray< int > sa;
    HRESULT hr = CreateANewSafeArray(sa.GetSafeArrayPtr());
}

However, this use bypasses the runtime type check in the Attach method and is not recommended for this reason.


previous page
next page
Converted from CHM to HTML with chm2web Pro 2.75 (unicode)