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.
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.
|