Chapter 3. ATL Smart Types
VARIANTs, SAFEARRAYs, and Interface Pointers
COM
has a number of data types beyond the numeric types available in
the C and C++ languages. Three such data types are the
VARIANT
data type, interface pointers, and the
SAFEARRAY
data type. ATL provides useful classes that
encapsulate each of these data types and their special
idiosyncrasies.
The CComVariant
class is a smart
VARIANT
class. The class implements the special
initialization, copy, and destruction semantics of the COM
VARIANT
data type. CComVariant
instances can be
used in most, but not all, of the places you would use a
VARIANT
.
The CComPtr
, CComQIPtr
, and
CComGITPtr
classes are smart pointer classes. Smart
pointer classes are definitions of objects that “act” like a
pointer; specifically, a pointer with extra semantics. The major
additional semantic for the ATL smart pointer classes is automatic
reference counting, which eliminates entire classes of errors.
The CComSafeArray
class manages the
Automation array type SAFEARRAY
. These arrays require
special handling with dedicated API functions for allocating,
deallocating, and interrogating members of the array.
The CComVariant Smart VARIANT Class
A Review of the COM VARIANT Data Type
Occasionally while using COM, you’ll want to pass parameters to a method without any knowledge of the data types the method requires. For the method to be capable of interpreting its received parameters, the caller must specify the format of the data as well as its value.
Alternatively, you can call a method that
returns a result that consists of varying data types, depending on
context. Sometimes it returns a string, sometimes a long
,
and sometimes even an interface pointer. This requires the method
to return data in a self-describing data format. For each value
transmitted, you send two fields: a code specifying a data type and
a value represented in the specified data type. Clearly, for this
to work, the sender and receiver must agree on the set of possible
formats.
COM specifies one such set of possible formats
(the VARTYPE
enumeration) and specifies the structure that
contains both the format and an associated value (the
VARIANT
structure). The VARIANT
structure looks
like this:
1typedef struct FARSTRUCT tagVARIANT VARIANT;
2typedef struct FARSTRUCT tagVARIANT VARIANTARG;
3
4typedef struct tagVARIANT {
5 VARTYPE vt;
6 unsigned short wReserved1;
7 unsigned short wReserved2;
8 unsigned short wReserved3;
9 union {
10 unsigned char bVal; // VT_UI1
11 short iVal; // VT_I2
12 long lVal; // VT_I4
13 float fltVal; // VT_R4
14 double dblVal; // VT_R8
15 VARIANT_BOOL boolVal; // VT_BOOL
16 SCODE scode; // VT_ERROR
17 CY cyVal; // VT_CY
18 DATE date; // VT_DATE
19 BSTR bstrVal; // VT_BSTR
20 IUnknown FAR* punkVal; // VT_UNKNOWN
21 IDispatch FAR* pdispVal; // VT_DISPATCH
22 SAFEARRAY FAR* parray; // VT_ARRAY|*
23 unsigned char FAR* pbVal; // VT_BYREF|VT_UI1
24 short FAR* piVal; // VT_BYREF|VT_I2
25 long FAR* plVal; // VT_BYREF|VT_I4
26 float FAR* pfltVal; // VT_BYREF|VT_R4
27 double FAR* pdblVal; // VT_BYREF|VT_R8
28 VARIANT_BOOL FAR* pboolVal; // VT_BYREF|VT_BOOL
29 SCODE FAR* pscode; // VT_BYREF|VT_ERROR
30 CY FAR* pcyVal; // VT_BYREF|VT_CY
31 DATE FAR* pdate; // VT_BYREF|VT_DATE
32 BSTR FAR* pbstrVal; // VT_BYREF|VT_BSTR
33 IUnknown FAR* FAR* ppunkVal; // VT_BYREF|VT_UNKNOWN
34 IDispatch FAR* FAR* ppdispVal; // VT_BYREF|VT_DISPATCH
35 SAFEARRAY FAR* FAR* pparray; // VT_ARRAY|*
36 VARIANT FAR* pvarVal; // VT_BYREF|VT_VARIANT
37 void FAR* byref; // Generic ByRef
38 };
39};
You initialize the VARIANT
structure by
storing a value in one of the fields of the tagged union and then
storing the corresponding type code for the value in the
vt
member of the VARIANT
structure. The
VARIANT
data type has a number of semantics that make
using it tedious and error prone for C++ developers.
Correctly using a VARIANT
requires that
you remember the following rules:
A
VARIANT
must be initialized before use by calling theVariantInit
function on it. Alternatively, you can initialize the type and associated value field to a valid state (such as setting thevt
field toVT_EMPTY
).A
VARIANT
must be copied by calling theVariantCopy
function on it. This performs the proper shallow or deep copy, as appropriate for the data type stored in theVARIANT
.A
VARIANT
must be destroyed by calling theVariantClear
function on it. This performs the proper shallow or deep destroy, as appropriate for the data type stored in theVARIANT
. For example, when you destroy aVARIANT
containing aSAFEARRAY
ofBSTR
s,VariantClear
frees eachBSTR
element in the array and then frees the array itself.A
VARIANT
can optionally represent, at most, one level of indirection, which is specified by adding theVT_BYREF
bit setting to the type code. You can callVariantCopyInd
to remove a single level of indirection from aVARIANT
.You can attempt to change the data type of a
VARIANT
by callingVariantChangeType[Ex]
.
With all these special semantics, it is useful
to encapsulate these details in a reusable class. ATL provides such
a class: CComVariant
.
The CComVariant Class
The CComVariant
class is an ATL utility
class that is a useful encapsulation for the COM self-describing
data type, VARIANT
. The atlcomcli.h
file contains
the definition of the CComVariant
class. The only state
the class maintains is an instance of the VARIANT
structure, which the class obtains by inheritance from the
tagVARIANT
structure. Conveniently, this means that a
CComVariant
instance is a VARIANT
structure, so
you can pass a CComVariant
instance to any function that
expects a VARIANT
structure.
1class CComVariant: public tagVARIANT {
2 ...
3};
You
often use a CComVariant
instance when you need to pass a
VARIANT
argument to a COM method. The following code
passes a string argument to a method that expects a
VARIANT
. The code uses the CComVariant
class to
do the conversion from a C-style string to the required
VARIANT
:
1STDMETHODIMP put_Name(/* [in] */ const VARIANT* name);
2
3HRESULT SetName (LPCTSTR pszName) {
4 // Initializes the VARIANT structure
5 // Allocates a BSTR copy of pszName
6 // Sets the VARIANT to the BSTR
7 CComVariant v (pszName);
8
9 // Pass the raw VARIANT to the method
10 return pObj->put_Name(&v);
11
12 // Destructor clears v freeing the BSTR
13}
Constructors and Destructor
Twenty-three constructors are available for
CComVariant
objects. The default constructor simply
invokes the COM function VariantInit
, which sets the
vt
flag to VT_EMPTY
and properly initializes the
VARIANT
so that it is ready to use. The destructor calls
the Clear
member function to release any resources
potentially held in the VARIANT
.
1CComVariant() {
2 ::VariantInit(this);
3}
4
5~CComVariant() {
6 Clear();
7}
The other 22 constructors initialize the
VARIANT
structure appropriately, based on the type of the
constructor argument.
Many of the constructors simply set the
vt
member of the VARIANT
structure to the value
representing the type of the constructor argument, and store the
value of the argument in the appropriate member of the union.
1CComVariant(BYTE nSrc) {
2 vt = VT_UI1;
3 bVal = nSrc;
4}
5CComVariant(short nSrc) {
6 vt = VT_I2;
7 iVal = nSrc;
8}
9CComVariant(long nSrc, VARTYPE vtSrc = VT_I4) {
10 ATLASSERT(vtSrc == VT_I4 || vtSrc == VT_ERROR);
11 vt = vtSrc;
12 lVal = nSrc;
13}
14CComVariant( float fltSrc) {
15 vt = VT_R4;
16 fltVal = fltSrc;
17}
A few of the
constructors are more complex. An SCODE
looks like a
long
to the compiler. Therefore, constructing a
CComVariant
that specifies an SCODE
or a
long
initialization value invokes the constructor that
accepts a long
. To enable you to distinguish these two
cases, this constructor also takes an optional argument that allows
you to specify whether the long
should be placed in the
VARIANT
as a long
or as an SCODE
. When
you specify a variant type other than VT_I4
or
VT_ERROR
, this constructor asserts in a debug build.
Windows 16-bit COM defined an HRESULT
(a handle to a result code) as a data type that contained an
SCODE
(a status code). Therefore, you’ll occasionally see
older legacy code that considers the two data types to be
different. In fact, some obsolete macros convert an SCODE
to an HRESULT
and extract the SCODE
from an
HRESULT
. However, the SCODE
and HRESULT
data types are identical in 32-bit COM applications. The
VARIANT
data structure contains an SCODE
field
instead of an HRESULT
because that’s the way it was
originally declared:
1CComVariant(long nSrc, VARTYPE vtSrc = VT_I4) ;
The constructor that accepts a bool
initialization value sets the contents of the VARIANT
to
VARIANT_TRUE
or VARIANT_FALSE
, as appropriate,
not to the bool
value specified. A logical TRUE
value, as represented in a VARIANT
, must be
VARIANT_TRUE
(1
as a 16-bit value), and logical
FALSE
is VARIANT_FALSE
(0
as a 16-bit
value). The Microsoft C++ compiler defines the bool
data
type as an 8-bit (0
or 1
) value. This constructor
provides the conversion between the two representations of a
Boolean value:
1CComVariant(bool bSrc) {
2 vt = VT_BOOL;
3 boolVal = bSrc ? ATL_VARIANT_TRUE : ATL_VARIANT_FALSE;
4}
Two constructors accept
an interface pointer as an initialization value and produce a
CComVariant
instance that contains an AddRef
’ed
copy of the interface pointer. The first constructor accepts an
IDispatch*
argument. The second accepts an
IUnknown*
argument.
1CComVariant(IDispatch* pSrc) ;
2CComVariant(IUnknown* pSrc) ;
Two constructors enable you to initialize a
CComVariant
instance from another VARIANT
structure or CComVariant
instance:
1CComVariant(const VARIANT& varSrc) {
2 vt = VT_EMPTY; InternalCopy (&varSrc);
3}
4CComVariant(const CComVariant& varSrc); { /* Same as above */ }
Both have identical implementations, which
brings up a subtle side effect, so let’s look at the
InternalCopy
helper function both constructors use:
1void InternalCopy(const VARIANT* pSrc) {
2 HRESULT hr = Copy(pSrc);
3 if (FAILED(hr)) {
4 vt = VT_ERROR;
5 scode = hr;
6#ifndef _ATL_NO_VARIANT_THROW
7 AtlThrow(hr);
8#endif
9 }
10}
Notice how InternalCopy
attempts to
copy the specified VARIANT
into the instance being
constructed; when the copy fails, InternalCopy
initializes
the CComVariant
instance as holding an error code
(VT_ERROR
). The Copy
method used to attempt the
copy returns the actual error code. This seems like an odd approach
until you realize that a constructor cannot return an error, short
of throwing an exception. ATL doesn’t require support for
exceptions, so this constructor must initialize the instance even
when the Copy
method fails. You can enable exceptions in
this code via the _ATL_NO_VARIANT_THROW
macro shown in the
code.
I once had a CComVariant
instance that
always seemed to have a VT_ERROR
code in it, even when I
thought it shouldn’t. As it turned out, I was constructing the
CComVariant
instance from an uninitialized
VARIANT
structure that resided on the stack. Watch out for
code like this:
1void func () { // The following code is incorrect
2 VARIANT v; // Uninitialized stack garbage in vt member
3 CComVariant sv(v); // Indeterminate state
4}
Three constructors accept a string
initialization value and produce a CComVariant
instance
that contains a BSTR
. The first constructor accepts a
CComBSTR
argument and creates a CComVariant
that
contains a BSTR
copied from the CComBSTR
argument. The second accepts an LPCOLESTR
argument and
creates a CComVariant
that contains a BSTR
that
is a copy of the specified string of OLECHAR
characters.
The third accepts an LPCSTR
argument and creates a
CComVariant
that contains a BSTR
that is a
converted-to-OLECHAR
copy of the specified ANSI character
string. These three constructors also possibly can “fail.” In all
three constructors, when the constructor cannot allocate memory for
the new BSTR
, it initializes the CComVariant
instance to VT_ERROR
with SCODE E_OUTOFMEMORY
.
The constructors also throw exceptions if the project settings are
set appropriately.
1CComVariant(const CComBSTR& bstrSrc);
2CComVariant(LPCOLESTR lpszSrc);
3CComVariant(LPCSTR lpszSrc);
Although both the first and second constructors
logically accept the same fundamental data type (a string of
OLECHAR
), only the first constructor properly handles
BSTR
s that contain an embedded NUL
and only then
if the CComBSTR
object supplied has been properly
constructed so that it has properly handled embedded NUL
s.
The following example makes this clear:
1LPCOLESTR osz = OLESTR("This is a\0BSTR string");
2BSTR bstrIn = ::SysAllocStringLen(osz, 21) ;
3// bstrIn contains "This is a\0BSTR string"
4
5CComBSTR bstr(::SysStringLen(bstrIn), bstrIn);
6
7CComVariant v1(bstr); // Correct v1 contains
8 // "This is a\0BSTR string"
9CComVariant v2(osz); // Wrong! v2 contains "This is a"
10
11::SysFreeString (bstrIn) ;
Assignment
The
CComVariant
class defines an army of assignment
operators: 33 in all. All the assignment operators do the following
actions:
Clear the variant of its current contents
Set the
vt
member of theVARIANT
structure to the value representing the type of the assignment operator argumentStore the value of the argument in the appropriate member of the union
1CComVariant& operator=(char cSrc); // VT_I1
2CComVariant& operator=(int nSrc); // VT_I4
3CComVariant& operator=(unsigned int nSrc); // VT_UI4
4CComVariant& operator=(BYTE nSrc); // VT_UI1
5CComVariant& operator=(short nSrc); // VT_I2
6CComVariant& operator=(unsigned short nSrc); // VT_UI2
7CComVariant& operator=(long nSrc); // VT_I4
8CComVariant& operator=(unsigned long nSrc); // VT_UI4
9
10CComVariant& operator=(LONGLONG nSrc); // VT_I8
11CComVariant& operator=(ULONGULONG nSrc); // VT_UI8
12CComVariant& operator=(float fltSrc); // VT_R4
13CComVariant& operator=(double dblSrc); // VT_R8
14CComVariant& operator=(CY cySrc); // VT_CY
Other overloads accept a pointer to all the
previous types. Internally, these produce a vt
value that
is the bitwise OR
of VT_BYREF
and the vt
member listed earlier. For example, operator=(long*)
sets
vt
to VT_I4 | VT_BYREF
.
The remaining operator=
methods have
additional semantics. Like the equivalent constructor, the
assignment operator that accepts a bool
initialization
value sets the contents of the VARIANT
to
VARIANT_TRUE
or VARIANT_FALSE
, as appropriate,
not to the bool
value specified.
1CComVariant& operator=(bool bSrc) ;
Two assignment operators accept an interface
pointer and produce a CComVariant
instance that contains
an AddRef
’ed copy of the interface pointer. One assignment
operator accepts an IDispatch*
argument. Another accepts
an IUnknown*
argument.
1CComVariant& operator=(IDispatch* pSrc) ;
2CComVariant& operator=(IUnknown* pSrc) ;
Two assignment operators allow you to initialize
a CComVariant
instance from another VARIANT
structure or CComVariant
instance. They both use the
InternalCopy
method, described previously, to make a copy
of the provided argument. Therefore, these assignments can “fail”
and produce an instance initialized to the VT_ERROR
type
(and possibly an exception).
1CComVariant& operator=(const CComVariant& varSrc);
2CComVariant& operator=(const VARIANT& varSrc);
One version of operator= accepts a
SAFEARRAY
as an argument:
1CComVariant& operator=(const SAFEARRAY *pSrc);
This method uses the COM API function
SafeArrayCopy
to produce an independent duplicate of the
SAFEARRAY
passed in. It properly sets the vt
member to be the bitwise OR
of VT_ARRAY
and the
vt
member of the elements of pSrc
.
The remaining three assignment operators accept
a string initialization value and produce a CComVariant
instance that contains a BSTR
. The first assignment
operator accepts a CComBSTR
argument and creates a
CComVariant
that contains a BSTR
that is a copy
of the specified CComBSTR
’s string data. The second
accepts an LPCOLESTR
argument and creates a
CComVariant
that contains a BSTR
that is a copy
of the specified string of OLECHAR
characters. The third
accepts an LPCSTR
argument and creates a
CComVariant
that contains a BSTR
that is a
converted-to- OLECHAR
copy of the specified ANSI character
string.
1CComVariant& operator=(const CComBSTR& bstrSrc);
2CComVariant& operator=(LPCOLESTR lpszSrc);
3CComVariant& operator=(LPCSTR lpszSrc);
The previous remarks about the constructors with a string initialization value apply equally to the assignment operators with a string initialization value. In fact, the constructors actually use these assignment operators to perform their initialization.
One final option to set the value of a
CComVariant
is the SetByRef
method:
1template< typename T >
2void SetByRef( T* pT ) {
3 Clear();
4 vt = CVarTypeInfo< T >::VT|VT_BYREF;
5 byref = pT;
6}
This method clears the current contents and then
sets the vt
field to the appropriate type with the
addition of the VT_BYREF
flag; this indicates that the
variant contains a pointer to the actual data, not the data itself.
The CVarTypeInfo
class is another traits class. The generic version follows:
1template< typename T >
2class CVarTypeInfo {
3// VARTYPE corresponding to type T
4// static const VARTYPE VT;
5// Pointer-to-member of corresponding field in VARIANT struct
6// static T VARIANT::* const pmField;
7};
The comments indicate what each template
specialization does: provides the VT
constant that gives
the appropriate VARTYPE
value to use for this type and the
pmField
pointer that indicates which field of a variant
stores this type. These are two of the specializations:
1template<>
2class CVarTypeInfo< unsigned char > {
3public:
4 static const VARTYPE VT = VT_UI1;
5 static unsigned char VARIANT::* const pmField;
6};
7
8__declspec( selectany ) unsigned char VARIANT::* const
9 CVarTypeInfo< unsigned char >::pmField = &VARIANT::bVal;
10
11template<>
12class CVarTypeInfo< BSTR > {
13public:
14 static const VARTYPE VT = VT_BSTR;
15 static BSTR VARIANT::* const pmField;
16};
17__declspec( selectany ) BSTR VARIANT::* const
18 CVarTypeInfo< BSTR >::pmField = &VARIANT::bstrVal;
The initializers are
necessary to set the pmField
pointers appropriately. The
pmField
value is not currently used anywhere in ATL 8, but
it could be useful for writing your own code that deals with
VARIANT
s.
CComVariant Operations
It’s important to realize that a
VARIANT
is a resource that must be managed properly. Just
as memory from the heap must be allocated and freed, a
VARIANT
must be initialized and cleared. Just as the
ownership of a memory block must be explicitly managed so it’s
freed only once, the ownership of the contents of a
VARIANT
must be explicitly managed so it’s cleared only
once. Four methods give you control over any resources a
CComVariant
instance owns.
The Clear
method releases any resources
the instance contains by calling the VariantClear
function. For an instance that contains, for example, a
long
or a similar scalar value, this method does nothing
except set the variant type field to VT_EMPTY
. However,
for an instance that contains a BSTR
, the method releases
the string. For an instance that contains an interface pointer, the
method releases the interface pointer. For an instance that
contains a SAFEARRAY
, this method iterates over each
element in the array, releasing each element, and then releases the
SAFEARRAY
itself.
1HRESULT Clear() { return ::VariantClear(this); }
When you no longer need the resource contained
in a CComVariant
instance, you should call the
Clear
method to release it. The destructor does this
automatically for you. This is one of the major advantages of using
CComVariant
instead of a raw VARIANT
: automatic
cleanup at the end of a scope. So, if an instance will quickly go
out of scope when you’re finished using its resources, let the
destructor take care of the cleanup. However, a
CComVariant
instance as a global or static variable
doesn’t leave scope for a potentially long time. The Clear
method is useful in this case.
The Copy
method makes a unique copy of
the specified VARIANT
. The Copy
method produces a
CComVariant
instance that has a lifetime that is separate
from the lifetime of the VARIANT
that it copies.
1HRESULT Copy(const VARIANT* pSrc)
2 { return ::VariantCopy(this, const_cast<VARIANT*>(pSrc)); }
Often, you’ll use the Copy
method to
copy a VARIANT
that you receive as an [in]
parameter. The caller providing an [in]
parameter is
loaning the resource to you. When you want to hold the parameter
longer than the scope of the method call, you must copy the
VARIANT
:
1STDMETHODIMP SomeClass::put_Option (const VARIANT* pOption) {
2 // Option saved in member m_Option of type CComVariant
3 return m_varOption.Copy (pOption) ;
4}
When you want to transfer ownership of the
resources in a CComVariant
instance from the instance to a
VARIANT
structure, use the Detach
method. It
clears the destination VARIANT
structure, does a
memcpy
of the CComVariant
instance into the
specified VARIANT
structure, and then sets the instance to
VT_EMPTY
. Note that this technique avoids extraneous
memory allocations and AddRef
/ Release
calls.
1HRESULT Detach(VARIANT* pDest);
Do not use the
Detach
method to update
an [out] VARIANT
argument
without special care! An [out]
parameter is
uninitialized on input to a method. The Detach
method
clears the specified VARIANT
before overwriting it.
Clearing a VARIANT
filled with random bits produces random
behavior.
1STDMETHODIMP SomeClass::get_Option (VARIANT* pOption) {
2 CComVariant varOption ;
3 ... Initialize the variant with the output data
4
5 // Wrong! The following code can generate an exception,
6 // corrupt your heap, and give at least seven years bad luck!
7 return varOption.Detach (pOption);
8}
Before detaching into an [out] VARIANT
argument, be sure to initialize the output argument:
1// Special care taken to initialize [out] VARIANT
2::VariantInit (pOption) ;
3// or
4pOption->vt = VT_EMPTY ;
5
6return vOption.Detach (pOption); // Now we can Detach safely.
When you want to transfer ownership of the
resources in a VARIANT
structure from the structure to a
CComVariant
instance, use the Attach
method. It
clears the current instance, does a memcpy
of the
specified VARIANT
into the current instance, and then sets
the specified VARIANT
to VT_EMPTY
. Note that this
technique avoids extraneous memory allocations and
AddRef
/ Release
calls.
1HRESULT Attach(VARIANT* pSrc);
Client code can use the Attach
method
to assume ownership of a VARIANT
that it receives as an
[out]
parameter. The function providing an [out]
parameter transfers ownership of the resource to the caller.
1STDMETHODIMP SomeClass::get_Option (VARIANT* pOption);
2
3void VerboseGetOption () {
4 VARIANT v;
5 pObj->get_Option (&v) ;
6
7 CComVariant cv;
8 cv.Attach (&v); // Destructor now releases the VARIANT
9}
Somewhat more efficiently, but potentially more dangerously, you could code this differently:
1void FragileGetOption() {
2 CComVariant v; // This is fragile code!!
3 pObj->get_Option (&v) ; // Directly update the contained
4 // VARIANT. Destructor now releases
5 // the VARIANT.
6}
Note that, in this case, the get_Option
method overwrites the VARIANT
structure contained in the
CComVariant
instance. Because the method expects an
[out]
parameter, the get_Option
method does not
release any resources contained in the provided argument. In the
preceding example, the instance was freshly constructed, so it is
empty when overwritten. The following code, however, causes a
memory leak:
1void LeakyGetOption() {
2 CComVariant v (OLESTR ("This string leaks!")) ;
3 pObj->get_Option (&v) ; // Directly updates the contained
4 // VARIANT. Destructor now releases
5 // the VARIANT.
6}
When you use a
CComVariant
instance as an [out]
parameter to a
method that expects a VARIANT
, you must first clear the
instance if there is any possibility that the instance is not
empty.
1void NiceGetOption() {
2 CComVariant v (OLESTR ("This string doesn't leak!")) ;
3 ...
4 v.Clear ();
5 pObj->get_Option (&v) ; // Directly updates the contained
6 // VARIANT. Destructor now releases
7 // the VARIANT.
8
9}
The ChangeType
method converts a
CComVariant
instance to the new type specified by the
vtNew
parameter. When you specify a second argument,
ChangeType
uses it as the source for the conversion.
Otherwise, ChangeType
uses the CComVariant
instance as the source for the conversion and performs the
conversion in place.
1HRESULT ChangeType(VARTYPE vtNew, const VARIANT* pSrc = NULL);
ChangeType
converts between the
fundamental types (including numeric-to-string and
string-to-numeric coercions). ChangeType
coerces a source
that contains a reference to a type (that is, the VT_BYREF
bit is set) to a value by retrieving the referenced value.
ChangeType
always coerces an object reference to a value
by retrieving the object’s Value
property. This is the
property with the DISPID_VALUE DISPID
. The
ChangeType
method can be useful not only for COM
programming, but also as a general data type conversion
library.
ComVariant Comparison Operators
The operator==()
method compares a
CComVariant
instance for equality with the specified
VARIANT
structure:
1bool operator==(const VARIANT& varSrc) const ;
2bool operator!=(const VARIANT& varSrc) const ;
When the two operands have differing types, the
operator returns false
. If they are of the same type, the
implementation calls the VarCmp API function [1] to
do the proper comparison based on the underlying type. The
operator!=()
method returns the negation of the
operator==()
method.
Both the
operator<()
and operator>()
methods perform
their respective comparisons using the Variant Math API function
VarCmp
:
1bool operator<(const VARIANT& varSrc) const ;
2bool operator>(const VARIANT& varSrc) const ;
CComVariant Persistence Support
You can use the last three methods of the
CComVariant
class to read and write a VARIANT
to
and from a stream:
1HRESULT WriteToStream(IStream* pStream);
2HRESULT ReadFromStream(IStream* pStream);
3ULONG GetSize() const;
The WriteToStream
method writes the
vt
type code to the stream. For simple types such as
VT_I4
, VT_R8
, and similar scalar values, it
writes the value of the VARIANT
to the stream immediately
following the type code. For an interface pointer,
WriteToStream
writes the GUID CLSID_NULL
to the
stream when the pointer is NULL
. When the interface
pointer is not NULL
, WriteToStream
queries the
referenced object for its IPersistStream
interface. If
that fails, it queries for IPersistStreamInit
. When the
object supports one of these interfaces, WriteToStream
calls the COM OleSaveToStream
function to save the object
to the stream. When the interface pointer is not NULL
and
the object does not support either the IPersistStream
or
IPersistStreamInit
interfaces, WriteToStream
fails.
For complex types, including VT_BSTR
,
all by-reference types, and all arrays, WriteToStream
attempts to convert the value, if necessary, to a BSTR
and
writes the string to the stream using
CComBSTR::WriteToStream
.
The ReadFromStream
method performs the
inverse operation. First, it clears the current
CComVariant
instance. Then it reads the variant type code
from the stream. For the simple types, such as VT_I4
,
VT_R8
, and similar scalar values, it reads the value of
the VARIANT
from the stream. For an interface pointer, it
calls the COM OleLoadFromStream
function to read the
object from the stream, requesting the IUnknown
or
IDispatch
interface, as appropriate. When
OleLoadFromStream
returns REGDB_E_CLASSNOTREG
(usually because of reading CLSID_NULL
),
ReadFromStream
silently returns an S_OK
status.
For all other types, including VT_BSTR
,
all by-reference types, and all arrays, ReadFromStream
calls CComBSTR::ReadFromStream
to read the previously
written string from the stream. The method then coerces the string
back to the original type.
GetSize
provides
an estimate of the amount of space that the variant will require in
the stream. This is needed for implementing various methods on the
persistence interfaces. The size estimate is exactly correct for
simple types (int, long, double, and so on). For variants that
contain interface pointers, CComVariant
does a
QueryInterface
for the IPersistStream
or
IPersistStreamInit
interfaces and uses the GetSizeMax( )
method to figure out the size. For other types, the variant
is converted to a BSTR
and the length of the string is
used.
In general, the GetSize( )
method is
used as an initial estimate for things such as buffer sizes. When
the code inside ATL calls GetSize( )
(mainly in persistence
support, discussed in Chapter 7, “Persistence in ATL”) it correctly
grows the buffers if the estimate is low. In your own use, be aware
that sometimes the GetSize( )
method won’t be exactly
correct.
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
VARIANT
s are self-describing generic data types,
SAFEARRAY
s are self-describing generic arrays.
SAFEARRAY
s are declared in IDL as follows:
1interface IMyInterface : IUnknown {
2 HRESULT GetArray([out,retval]
3 SAFEARRAY(VARIANT_BOOL)* myArray);
4};
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:
1typedef struct tagSAFEARRAY {
2 USHORT cDims;
3 USHORT fFeatures;
4 ULONG cbElements;
5 ULONG cLocks;
6 PVOID pvData;
7 SAFEARRAYBOUND rgsabound[ 1 ];
8} SAFEARRAY;
The upper and lower bounds for the
SAFEARRAY
are stored in the rgsabound
array. Each
element in this array is a SAFEARRAYBOUND
structure.
1typedef struct tagSAFEARRAYBOUND {
2 ULONG cElements;
3 LONG lLbound;
4} 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
SAFEARRAY
s. These functions enable you to create, access,
and destroy SAFEARRAY
s of various dimensions and sizes.
The following code demonstrates how to use these functions to
manipulate two-dimensional SAFEARRAY
s of double. The first
step is to create an array of SAFEARRAYBOUND
structures to
indicate the number and size of the array dimensions:
1SAFEARRAYBOUND rgsabound[2];
2rgsabound[0].cElements = 3;
3rgsabound[0].lLbound = 0;
4rgsabound[1].cElements = 4;
5rgsabound[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:
1SAFEARRAY* 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:
1long rgIndices[] = { 2, 1 };
2double lElem;
3::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.
1::SafeArrayDestroy(psa);
As you can see, manipulating SAFEARRAY
s
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:
1template <typename T,
2 VARTYPE _vartype = _ATL_AutomationType<T>::type>
3class CComSafeArray {
4 ...
5public:
6 LPSAFEARRAY m_psa;
7}
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 SAFEARRAY
s
can hold only Automation-compatible types as elements; that 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 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The documentation indicates that the last three
VARTYPEs
– BSTR
, IDispatch
, and
IUnknown
pointers – are 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:
1template <typename T>
2struct _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:
1#define DEFINE_AUTOMATION_TYPE_FUNCTION(ctype,
2 typewrapper, oleautomationtype) \
3 template <> \
4 struct _ATL_AutomationType<ctype> { \
5 typedef typewrapper _typewrapper;\
6 enum { type = oleautomationtype }; \
7 static void* GetT(const T& t) { \
8 return (void*)&t; \
9 } \
10};
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 *
.
1DEFINE_AUTOMATION_TYPE_FUNCTION(CHAR, CHAR, VT_I1)
2DEFINE_AUTOMATION_TYPE_FUNCTION(SHORT, SHORT, VT_I2)
3DEFINE_AUTOMATION_TYPE_FUNCTION(INT, INT, VT_I4)
4DEFINE_AUTOMATION_TYPE_FUNCTION(LONG, LONG, VT_I4)
5DEFINE_AUTOMATION_TYPE_FUNCTION(LONGLONG, LONGLONG, VT_I8)
6DEFINE_AUTOMATION_TYPE_FUNCTION(BYTE, BYTE, VT_UI1)
7DEFINE_AUTOMATION_TYPE_FUNCTION(USHORT, USHORT, VT_UI2)
8DEFINE_AUTOMATION_TYPE_FUNCTION(UINT, UINT, VT_UI4)
9DEFINE_AUTOMATION_TYPE_FUNCTION(ULONG, ULONG, VT_UI4)
10DEFINE_AUTOMATION_TYPE_FUNCTION(ULONGLONG, ULONGLONG, VT_UI8)
11DEFINE_AUTOMATION_TYPE_FUNCTION(FLOAT, FLOAT, VT_R4)
12DEFINE_AUTOMATION_TYPE_FUNCTION(DOUBLE, DOUBLE, VT_R8)
13DEFINE_AUTOMATION_TYPE_FUNCTION(DECIMAL, DECIMAL, VT_DECIMAL)
14DEFINE_AUTOMATION_TYPE_FUNCTION(VARIANT, CComVariant, VT_VARIANT)
15DEFINE_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
:
1explicit 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.
1class CComSafeArrayBound : public SAFEARRAYBOUND {
2 CComSafeArrayBound(ULONG ulCount = 0, LONG lLowerBound = 0)
3 { ... }
4 CComSafeArrayBound&
5 operator=(const CComSafeArrayBound& bound)
6 { ... }
7 CComSafeArrayBound& operator=(ULONG ulCount) { ... }
8 ULONG GetCount() const { ... }
9 ULONG SetCount(ULONG ulCount) { ... }
10 LONG GetLowerBound() const { ... }
11 LONG SetLowerBound(LONG lLowerBound) { ... }
12 LONG GetUpperBound() const { ... }
13};
A quick look at the implementation for the
CComSafeArray(ULONG, LONG)
constructor demonstrates how
all the nondefault constructors use the CComSafeArrayBound
wrapper class:
1explicit CComSafeArray(ULONG ulCount, LONG lLBound = 0)
2 : m_psa(NULL) {
3 CComSafeArrayBound bound(ulCount, lLBound);
4 HRESULT hRes = Create(&bound);
5 if (FAILED(hRes))
6 AtlThrow(hRes);
7}
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:
1HRESULT Create(const SAFEARRAYBOUND *pBound, UINT uDims = 1) {
2 ATLASSERT(m_psa == NULL);
3 ATLASSERT(uDims > 0);
4 HRESULT hRes = S_OK;
5 m_psa = SafeArrayCreate(_vartype, uDims,
6 const_cast<LPSAFEARRAYBOUND>(pBound));
7 if (NULL == m_psa)
8 hRes = E_OUTOFMEMORY;
9 else
10 hRes = Lock();
11 return hRes;
12}
This first constructor just shown is probably
the most frequently used. One-dimensional SAFEARRAY
s are
much more common than multidimensional SAFEARRAY
s, and C++
developers are accustomed to zero-based array indexing. You make
use of this simple constructor with code such as the following:
1// create a 1-D zero-based SAFEARRAY of long with 10 elements
2CComSafeArray<long> sa(10);
3
4// create a 1-D one-based SAFEARRAY of double with 5 elements
5CComSafeArray<double> sa(5,1);
The second CComSafeArray
constructor
enables you to pass in a SAFEARRAYBOUND
structure or a
CComSafeArrayBound
instance:
1explicit CComSafeArray(const SAFEARRAYBOUND& bound);
This constructor is invoked when you write code similar to the following:
1CComSafeArrayBound bound(5,1); // 1-D one-based array
2CComSafeArray<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:
1explicit CComSafeArray(const SAFEARRAYBOUND *pBound, UINT uDims = 1);
You create a multidimensional
CComSafeArray
with this constructor as follows:
1// 3-D array with all dimensions
2// left-most dimension has 3 elements
3CComSafeArrayBound bound1(3);
4// middle dimension has 4 elements
5CComSafeArrayBound bound2(4);
6// right-most dimension has 5 elements
7CComSafeArrayBound bound3(5);
8
9// equivalent C-style array indices would be [3][4][5]
10CComSafeArrayBound rgBounds[] = { bound1, bound2, bound3 };
11CComSafeArray<int> sa(rgBounds, 3);
Note that nothing prevents you from creating
different starting indices for the different dimensions of the
SAFEARRAY``nothing but your conscience that is. This would
be extraordinarily confusing for any code that uses this type. In
any event, as mentioned previously, multidimensional
``SAFEARRAY
s 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:
1CComSafeArray(const SAFEARRAY *psaSrc) : m_psa(NULL);
2CComSafeArray(const SAFEARRAY& saSrc) : m_psa(NULL);
3CComSafeArray(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:
1CComSafeArray<int> saSrc(5); // source is 1-D array of 5 ints
2// allocate storage for 1-D array of 5 ints
3// and copy contents of source
4CComSafeArray<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:
1HRESULT Destroy() {
2 HRESULT hRes = S_OK;
3 if (m_psa != NULL) {
4 hRes = Unlock();
5 if (SUCCEEDED(hRes)) {
6 hRes = SafeArrayDestroy(m_psa);
7 if (SUCCEEDED(hRes))
8 m_psa = NULL;
9 }
10 }
11 return hRes;
12}
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
SAFEARRAY
s 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:
1CComSafeArray<T>& operator=(const CComSafeArray& saSrc) {
2 *this = saSrc.m_psa;
3 return *this;
4}
5CComSafeArray<T>& operator=(const SAFEARRAY *psaSrc) {
6 ATLASSERT(psaSrc != NULL);
7HRESULT hRes = CopyFrom(psaSrc);
8 if (FAILED(hRes))
9 AtlThrow(hRes);
10 return *this;
11}
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:
1CComSafeArray<long> sa1(10);
2// do something interesting with sa1
3CComSafeArray<long> sa2(5);
4
5// free contents of sa1, duplicate contents
6// of sa2 and put into sa1
7sa1 = 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
:
1HRESULT Attach(const SAFEARRAY *psaSrc) {
2 ATLENSURE_THROW(psaSrc != NULL, E_INVALIDARG);
3
4 VARTYPE vt;
5 HRESULT hRes = ::ATL::AtlSafeArrayGetActualVartype(
6 const_cast<LPSAFEARRAY>(psaSrc), &vt);
7 ATLENSURE_SUCCEEDED(hRes);
8 ATLENSURE_THROW(vt == GetType(), E_INVALIDARG);
9 hRes = Destroy();
10 m_psa = const_cast<LPSAFEARRAY>(psaSrc);
11 hRes = Lock();
12 return hRes;
13}
14
15LPSAFEARRAY Detach() {
16 Unlock();
17 LPSAFEARRAY pTemp = m_psa;
18 m_psa = NULL;
19 return pTemp;
20}
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:
1SAFEARRAY *psa = ::SafeArrayCreateVector(VT_I4, 0, 10);
2// BAD - this pointer may not be valid!
3int *pData = reinterpret_cast<int *>(pda->pvData);
4// BOOM (maybe)
5pData[0] = 5;
Instead, you need to first lock the
SAFEARRAY
:
1SAFEARRAY *psa = ::SafeArrayCreateVector(VT_I4, 0, 10);
2// GOOD - this will allocate the actual storage for the data
3::SafeArrayLock(psa);
4// Now the pointer is valid
5int *pData = ( int * )(pda->pvData);
6pData[0] = 5;
7// Unlock after we're done
8::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:
1STDMETHODIMP SomeClass::AverageArray(/* [in] */ SAFEARRAY* psa,
2 /* [out] */ LONG* plAvg) {
3 if (!plAvg) return E_POINTER;
4 CComSafeArray<long> sa; // Note: no type check is done
5 // against psa type
6 sa.Attach(psa); // we're pointing at the same
7 // memory as psa
8
9 ... perform some calculations
10
11 sa.Detach(); // Must detach here or risk a crash
12 return S_OK;
13}
When you want to return a SAFERRAY
from
a method call, turn to the Detach
operation, as in the
following example:
1STDMETHODIMP SomeClass::get_Array(/* [out] */ SAFEARRAY** ppsa) {
2 if (!ppsa) return E_POINTER;
3 CComSafeArray<long> sa(10);
4
5 ... populate sa instance
6
7 // no resources released when we leave scope
8 // and no copying performed
9 *ppsa = sa.Detach();
10 return S_OK;
11}
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:
1STDMETHODIMP SomeClass::DontDoThis(SAFEARRAY* psa) {
2 // We have two references to the safearray
3 CComSafeArray<long> sa1, sa2;
4 sa1.Attach(psa);
5 sa2.Attach(psa);
6
7 // manipulate the array here
8 // BUG: Don't do this
9 sa2.Destroy( );
10}
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:
1LONG GetLowerBound(UINT uDim = 0) const;
2LONG GetUpperBound(UINT uDim = 0) const;
3ULONG GetCount(UINT uDim = 0) const;
4UINT GetDimensions() const;
5VARTYPE GetType() const ;
6bool 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.
1typedef struct tagSAFEARRAY {
2 ...
3 USHORT fFeatures;
4 ...
5} 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:
1LPSAFEARRAY* GetSafeArrayPtr() {
2 return &m_psa;
3}
If a CComSafeArray
instance is
resizable, clients can use two different Resize
methods to
grow or shrink a SAFEARRAY
:
1HRESULT Resize(ULONG ulCount, LONG lLBound = 0);
2HRESULT 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:
1HRESULT Resize(const SAFEARRAYBOUND *pBound) {
2 ATLASSUME(m_psa != NULL);
3 ATLASSERT(pBound != NULL);
4 if (!IsSizable()) {
5 return E_FAIL;
6 }
7 HRESULT hRes = Unlock();
8 if (SUCCEEDED(hRes)) {
9 hRes = SafeArrayRedim(m_psa, const_cast<LPSAFEARRAYBOUND>(pBound));
10 if (SUCCEEDED(hRes)) {
11 hRes = Lock();
12 }
13 }
14 return hRes;
15}
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 SAFEARRAY
s. All three versions will
assert if the Add
method is invoked on a
CComSafeArray
instance that contains a multidimensional
SAFEARRAY
.
1HRESULT Add(const T& t, BOOL bCopy = TRUE);
2HRESULT Add(ULONG ulCount, const T *pT, BOOL bCopy = TRUE);
3HRESULT 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:
1CComSafeArray<int> sa; // sa::m_psa is NULL
2sa.Add(7); // sa allocated and now contains { 7 }
3
4int rgVal[] = { 8, 9 };
5sa.Add(2, rgVal); // sa now contains { 7, 8, 9 }
6
7sa.Add(sa); // sa now contains { 7, 8, 9, 7, 8, 9 }
8 // see discussion of cast operators to
9 // 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 SAFEARRAY
s. The GetAt
method
comes in two flavors.
1const typename _ATL_AutomationType<T>::_typewrapper&
2GetAt(LONG lIndex) const {
3 ATLASSUME(m_psa != NULL);
4 if(m_psa == NULL)
5 AtlThrow(E_FAIL);
6 LONG lLBound = GetLowerBound();
7 ATLASSERT(lIndex >= lLBound);
8 ATLASSERT(lIndex <= GetUpperBound());
9 if ((lIndex < lLBound) || (lIndex > GetUpperBound()))
10 AtlThrow(E_INVALIDARG);
11
12 return ( (_ATL_AutomationType<T>::_typewrapper*)
13 m_psa->pvData )[lIndex-lLBound];
14}
15
16_ATL_AutomationType<T>::_typewrapper& GetAt(LONG lIndex) {
17 // code identical to const version
18}
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:
1template <>
2struct _ATL_AutomationType<BSTR> {
3 typedef CComBSTR _typewrapper ;
4 enum { type = VT_BSTR};
5 static void* GetT(const BSTR& t) {
6 return t;
7 }
8};
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 SAFEARRAY
s, 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:
1HRESULT SetAt(LONG lIndex, const T& t, BOOL bCopy = TRUE) {
2 bCopy;
3 ATLASSERT(m_psa != NULL);
4 LONG lLBound = GetLowerBound();
5 ATLASSERT(lIndex >= lLBound);
6 ATLASSERT(lIndex <= GetUpperBound());
7 ((T*)m_psa->pvData)[lIndex-lLBound] = t;
8 return S_OK;
9}
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:
1CComSafeArray<long> sa(5);
2long lNewVal = 14;
3// replace the 4th element with the value 14
4sa.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
SAFEARRAY
s: 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.
1template<>
2HRESULT CComSafeArray<BSTR>::SetAt(LONG lIndex,
3 const BSTR& strData, BOOL bCopy) {
4 // validation code omitted for clarity
5
6 BSTR strOrg = ((BSTR*)m_psa->pvData)[lIndex-lLBound];
7 if (strOrg)
8 ::SysFreeString(strOrg);
9
10 if (bCopy) {
11 BSTR strTemp = ::SysAllocString(strData);
12 if (NULL == strTemp)
13 return E_OUTOFMEMORY;
14 ((BSTR*)m_psa->pvData)[lIndex-lLBound] = strTemp;
15 }
16 else
17 ((BSTR*)m_psa->pvData)[lIndex-lLBound] = strData;
18 return S_OK;
19}
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:
1BSTR bstr1 = ::SysAllocString(OLESTR("Go Longhorns!"));
2BSTR bstr2 = ::SysAllocString(OLESTR("ATL Rocks!"));
3
4CComSafeArray<BSTR> sa(5);
5sa.SetAt(2, bstr1, true); // sa generates its own copy of bstr1
6sa.SetAt(3, bstr2, false); // sa assigns element to bstr2
7::SysFreeString(bstr1); // ok, sa still has a copy
8::SysFreeString(bstr2); // wrong!!! we don't own bstr2
VARIANT
elements in SAFEARRAY
s
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 different``pDisp`` 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:
1IUnknown* pUnk1, pUnk2;
2// assign both pointers to refer to an object
3CComSafeArray<IUnknown*> sa(5);
4sa.SetAt(2, pUnk1, true); // sa calls AddRef on pUnk1
5sa.SetAt(3, pUnk2, false); // sa assigns element to pUnk2
6 // without AddRefing
7pUnk1->Release(); // ok, refcount non-zero because
8 // of sa AddRef
9pUnk2->Release(); // wrong!!! we don't own pUnk2
The remaining two
methods for accessing SAFEARRAY
elements apply to
multidimensional SAFEARRAY
s. 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.
1HRESULT MultiDimGetAt(const LONG *alIndex, T& t) {
2 ATLASSERT(m_psa != NULL);
3 return SafeArrayGetElement(m_psa,
4 const_cast<LONG*>(alIndex), &t);
5}
6HRESULT MultiDimSetAt(const LONG *alIndex, const T& t) {
7 ATLASSERT(m_psa != NULL);
8 return SafeArrayPutElement(m_psa, const_cast<LONG*>(alIndex),
9 _ATL_AutomationType<T>::GetT(t));
10}
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:
1// 2-D array with all dimensions
2// left-most dimension has 3 elements
3CComSafeArrayBound bound1(3);
4// right-most dimension has 4 elements
5CComSafeArrayBound bound2(4);
6
7// equivalent C-style array indices would be [3][4]
8CComSafeArrayBound rgBounds[] = { bound1, bound2 };
9CComSafeArray<int> sa(rgBounds, 2);
10
11int rgIndElement1[] = { 0, 1 }; // access element at sa[1][0]
12int rgIndElement2[] = { 3, 2 }; // access element at sa[2][3]
13
14long lVal = 0;
15// retrieve value at sa[1][0]
16sa.MultiDimGetAt(rgIndElement1, lVal);
17
18// multiply value by 2 and store it
19// in element located at sa[2][3]
20sa.MultiDimSetAt(rgIndElement2, lVal*=2);
CComSafeArray Operators
CComSafeArray
defines four operators that provide some syntactic convenience for
accessing elements of a SAFEARRAY
:
1const typename
2_ATL_AutomationType<T>::_typewrapper&
3operator[](int nIndex) const {
4 return GetAt(nIndex);
5}
6typename
7_ATL_AutomationType<T>::_typewrapper&
8operator[](int nIndex) {
9 return GetAt(nIndex);
10}
11const typename
12_ATL_AutomationType<T>::_typewrapper&
13operator[](LONG nIndex)
14 const {
15 return GetAt(nIndex);
16}
17typename
18_ATL_AutomationType<T>::_typewrapper&
19operator[](LONG nIndex) {
20 return GetAt(nIndex);
21}
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.
1CComSafeArray<int> sa(5);
2ATLASSERT(sa[2] == 0);
3sa[2] = 17;
4ATLASSERT(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*
.
1operator const SAFEARRAY *() const {
2 return m_psa;
3}
4operator LPSAFEARRAY() {
5 return m_psa;
6}
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:
1HRESULT CreateANewSafeArray( SAFEARRAY** ppsa ) {
2 *ppsa = SafeArrayCreateVector(VT_I4, 1, 15 );
3 return S_OK;
4}
5HRESULT UseCreatedSafeArray( ) {
6 CComSafeArray< int > sa;
7 HRESULT hr = CreateANewSafeArray( &sa );
8}
The previous code will not compile but will fail with this error:
1error C2664: CreateANewSafeArray : cannot convert
2parameter 1 from
3 ATL::CComSafeArray<T> *__w64 to SAFEARRAY **
4 with
5 [
6 T=int
7 ]
8 Types pointed to are unrelated;
9 conversion requires reinterpret_cast,
10 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?
1HRESULT CreateANewSafeArray( SAFEARRAY** ppsa ) {
2 *ppsa = SafeArrayCreateVector(VT_BSTR, 1, 15 );
3 return S_OK;
4}
5
6HRESULT UseCreatedSafeArray( ) {
7 CComSafeArray< int > sa;
8 HRESULT hr = CreateANewSafeArray( &sa );
9}
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
BSTR
s. Nothing good can come of this, so the
operator&
overload was left out. Instead, you can do
this:
1HRESULT UseCreatedSafeArray( ) {
2 SAFEARRAY *psa = null;
3 HRESULT hr = CreateANewSafeArray( &psa );
4 CComSafeArray< int > sa;
5 sa.Attach( psa );
6}
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:
1HRESULT UseCreatedSafeArray( ) {
2 CComSafeArray< int > sa;
3 HRESULT hr = CreateANewSafeArray(sa.GetSafeArrayPtr());
4}
However, this use bypasses the runtime type
check in the Attach
method and is not recommended for this
reason.
The CComPtr and CComQIPtr Smart Pointer Classes
A Review of Smart Pointers
A smart pointer is an object that behaves like a pointer. That is, you can use an instance of a smart pointer class in many of the places you normally use a pointer. However, using a smart pointer provides some advantages over using a raw pointer. For example, a smart interface pointer class can do the following:
Release the encapsulated interface pointer when the class destructor executes.
Automatically release its interface pointer during exception handling when you allocate the smart interface pointer on the stack. This reduces the need to write explicit exception-handling code.
Release the encapsulated interface pointer before overwriting it during an assignment operation.
Call
AddRef
on the interface pointer received during an assignment operation.Provide different constructors to initialize a new smart pointer through convenient mechanisms.
Be used in many, but not all, the places where you would conventionally use a raw interface pointer.
ATL provides two smart pointer classes:
CComPtr
and CComQIPtr
. The CComPtr
class
is a smart COM interface pointer class. You create instances
tailored for a specific type of interface pointer. For example, the
first line of the following code creates a smart IUnknown
interface pointer. The second line creates a smart
INamedObject
custom interface pointer:
1CComPtr<IUnknown> punk;
2CComPtr<INamedObject> pno;
The CComQIPtr
class is a smarter COM
interface pointer class that does everything CComPtr
does
and more. When you assign to a CComQIPtr
instance an
interface pointer of a different type than the smart pointer, the
class calls QueryInterface
on the provided interface
pointer:
1CComPtr<IUnknown> punk = /* Init to some IUnknown* */ ;
2CComQIPtr<INamedObject> pno = punk; // Calls punk->QI
3 // (IID_INamedObject, ...)
The CComPtr and CComQIPtr Classes
The CComPtr
and CComQIPtr
classes are similar, with the exception of initialization and
assignment. In fact, they’re so similar that CComQIPtr
actually derives from CComPtr
and CComPtr
, in
turn, derives from another class, CComPtrBase
. This latter
class defines the actual storage of the underlying raw pointer, and
the actual reference-counting operations on that raw pointer.
CComPtr
and CComQIPtr
add constructors and
assignment operators. Because of the inheritance relationship
between these classes, all the following comments about the
CComPtr
class apply equally to the CComQIPtr
class unless specifically stated otherwise.
The atlcomcli.h
file contains the
definition of all three classes. The only state each class
maintains is a single public member variable, T* p
. This
state is defined in the CComPtrBase
base class:
1template <class T>
2class CComPtrBase {
3 ...
4 T* p;
5};
6
7template <class T>
8class CComPtr : public CComPtrBase<T>
9{ ... };
10
11template <class T, const IID* piid = &__uuidof(T)>
12class CComQIPtr : public CComPtr<T>
13{ ... };
The first (or, in the case of CComPtr
,
only) template parameter specifies the type of the smart interface
pointer. The second template parameter to the CComQIPtr
class specifies the interface ID for the smart pointer. By default,
it is the globally unique identifier (GUID) associated with the
class of the first parameter. Here are a few examples that use
these smart pointer classes. The middle three examples are all
equivalent:
1CComPtr<IUnknown> punk; // Smart IUnknown*
2CComPtr<INamedObject> pno; // Smart INamedObject*
3
4CComQIPtr<INamedObject> pno;
5CComQIPtr<INamedObject, &__uuidof(INamedObject)> pno;
6CComQIPtr<INamedObject, &IID_INamedObject> pno;
7
8CComQIPtr<IDispatch, &IID_ISomeDual> pdisp;
Constructors and Destructor
A CComPtr
object can be initialized
with an interface pointer of the appropriate type. That is, a
CComPtr<IFoo>
object can be initialized using an
IFoo*
or another CComPtr<IFoo>
object.
Using any other type produces a compiler error. The actual
implementation of this behavior is in the CComPtrBase
class. The default constructor initializes the internal interface
pointer to NULL
. The other constructors initialize
the internal interface
pointer to the specified interface pointer. When the specified
value is non- NULL
, the constructor calls the
AddRef
method. The destructor calls the Release
method on a non-NULL
interface pointer.
CComPtr
has a special copy constructor
that pulls out the underlying raw interface pointer and passes it
to the CComPtrBase
base class, thus guaranteeing proper
AddRef
and Release
calls.
1CComPtrBase() { p = NULL; }
2CComPtrBase(T* p) { if ((p = lp) != NULL) p->AddRef(); }
3~CComPtrBase() { if (p) p->Release(); }
4
5CComPtr(const CComPtr<T>& lp) : CComPtrBase<T>(lp.p) { }
A CComQIPtr
object can be initialized
with an interface pointer of any type. When the initialization
value is the same type as the smart pointer, the constructor simply
AddRef
’s the provided pointer via the base class’s
constructor:
1CComQIPtr(T* lp) :
2 CComPtr<T>(lp)
3 {}
4
5CComQIPtr(const CComQIPtr<T,piid>& lp) :
6 CComPtr<T>(lp.p)
7 {}
However, specifying a different type invokes the following constructor, which queries the provided interface pointer for the appropriate interface:
1CComQIPtr(IUnknown* lp)
2 { if (lp != NULL) lp->QueryInterface(*piid, (void **)&p); }
A constructor can never fail. Nevertheless, the
QueryInterface
call might not succeed. The
CComQIPtr
class sets the internal pointer to NULL
when it cannot obtain the required interface. Therefore, you use
code such as the following to test whether the object
initializes:
1void func (IUnknown* punk) {
2 CComQIPtr<INamedObject> pno (punk);
3 if (pno) {
4 // Can call SomeMethod because the QI worked
5 pno->SomeMethod ();
6 }
7}
You can tell whether the
query failed by checking for a NULL
pointer, but you
cannot determine why it fails. The constructor doesn’t save the
HRESULT
from a failed QueryInterface
call.
Initialization
The CComPtr
class defines three
assignment operators; the CComQIPtr
class defines three
slightly different ones. All the assignment operators do the same
actions:
Release
the current interface pointer when it’s non-NULL
.AddRef
the source interface pointer when it’s non-NULL
.Save the source interface pointer as the current interface pointer.
The CComPtr
assignment operators are
shown here:
1// CComPtr assignment operators
2T* operator=(T* lp);
3template <typename Q> T* operator=(const CComPtr<Q>& lp);
4T* operator=(const CComPtr<T>& lp);
The templated version of operator=
is
interesting. It enables you to assign arbitrary CComPtrs
to each other with proper QueryInterface
calls made, if
necessary. For example, this is now legal:
1CComPtr< IFoo > fooPtr = this;
2CComPtr< IBar > barPtr;
3
4barPtr = fooPtr;
This begs the question: Why have
CComQIPtr
at all if CComPtr
does the work, too?
It appears that the ATL team is moving toward having a single smart
interface pointer instead of two and is leaving CComQIPtr
in place for backward compatibility.
The CComQIPtr
assignment operators are
mostly the same. The only one that does any interesting work is the
overload that takes an IUnknown*.
This queries a
non- NULL
source interface pointer for the appropriate
interface to save. You receive a NULL
pointer when the
QueryInterface
calls fail. As with the equivalent
constructor, the HRESULT
for a failed query is not
available.
1// CComQIPtr assignment operators
2T* operator=(T* lp);
3T* operator=(const CComQIPtr<T>& lp);
4T* operator=(IUnknown* lp);
Typically, you use the CComQIPtr
assignment operator to perform a QueryInterface
call. You
immediately follow the assignment with a NULL
pointer
test, as follows:
1// Member variable holding object
2CComQIPtr<IExpectedInterface> m_object;
3
4STDMETHODIMP put_Object (IUnknown* punk) {
5 // Releases current object, if any, and
6 m_object = punk; // queries for the expected interface
7 if (!m_object)
8 return E_UNEXPECTED;
9 return S_OK;
10}
Object Instantiation Methods
The CComPtrBase
class provides an
overloaded method, called CoCreateInstance
, that you can
use to instantiate an object and retrieve an interface pointer on
the object. The method has two forms. The first requires the class
identifier (CLSID
) of the class to instantiate. The second
requires the programmatic identifier (ProgID
) of the class
to instantiate. Both overloaded methods accept optional parameters
for the controlling unknown and class context for the
instantiation. The controlling unknown parameter defaults to
NULL
, the normal case, which indicates no aggregation. The
class context parameter defaults to CLSCTX_ALL
, indicating
that any available server can service the request.
1HRESULT CoCreateInstance (REFCLSID rclsid,
2 LPUNKNOWN pUnkOuter = NULL,
3 DWORD dwClsContext = CLSCTX_ALL) {
4 ATLASSERT(p == NULL);
5 return ::CoCreateInstance(rclsid, pUnkOuter,
6 dwClsContext, __uuidof(T), (void**)&p);
7}
8
9HRESULT CoCreateInstance (LPCOLESTR szProgID,
10 LPUNKNOWN pUnkOuter = NULL,
11 DWORD dwClsContext = CLSCTX_ALL);
Notice how the preceding code for the first
CoCreateInstance
method creates an instance of the
specified class. It passes the parameters of the method to the
CoCreateInstance
COM API and, additionally, requests that
the initial interface be the interface that the smart pointer class
supports. (This is the purpose of the __uuidof(T)
expression.) The second overloaded CoCreateInstance
method
translates the provided ProgID
to a CLSID
and
then creates the instance in the same manner as the first
method.
Therefore, the following code is equivalent
(although the smart pointer code is easier to read, in my opinion).
The first instantiation request explicitly uses the
CoCreateInstance
COM API. The second uses the smart
pointer CoCreateInstance
method.
1ISpeaker* pSpeaker;
2HRESULT hr =
3 ::CoCreateInstance (__uuidof (Demagogue), NULL, CLSCTX_ALL,
4 __uuidof (ISpeaker_, (void**) &pSpeaker);
5//... Use the interface
6pSpeaker->Release () ;
7
8
9CComPtr<ISpeaker> pSpeaker;
10HRESULT hr = pSpeaker.CoCreateInstance (__uuidof (Demogogue));
11//... Use the interface. It releases when pSpeaker leaves scope
CComPtr and CComQIPtr Operations
Because a smart interface pointer should behave
as much as possible like a raw interface pointer, the
CComPtrBase
class defines some operators to make the smart
pointer objects act like pointers. For example, when you
dereference a pointer using operator*()
, you expect to
receive a reference to whatever the pointer points. So
dereferencing a smart interface pointer should produce a reference
to whatever the underlying interface pointer points to. And it
does:
1T& operator*() const { ATLENSURE(p!=NULL); return *p; }
Note that the operator*()
method kindly
asserts (via the ATLENSURE
macro [2] )
when you attempt to dereference a NULL
smart interface
pointer in a debug build of your component. Of course, I’ve always
considered the General Protection Fault message box to be an
equivalent assertion. However, the ATLENSURE
macro
produces a more programmer-friendly indication of the error
location.
ATLENSURE asserts if assertions are turned on and throws either a
C++ exception (if exceptions are enabled) or a Windows Structured
Exception (if C++ exceptions are disabled) if the condition is
false
.
To maintain the semblance of a pointer, taking
the address of a smart pointer object; that is, invoking
operator&()
should actually return the address of the
underlying raw pointer. Note that the issue here
isn’t the actual binary value returned. A smart pointer contains
only the underlying raw interface pointer as its state. Therefore,
a smart pointer occupies exactly the same amount of storage as a
raw interface pointer. The address of a smart pointer object and
the address of its internal member variable are the same binary
value.
Without overriding
CComPtrBase<T>::operator&()
, taking the address
of an instance returns a CComPtrBase<T>*
. To have a
smart pointer class maintain the same pointer semantics as a
pointer of type T*
, the operator&()
method
for the class must return a T**
.
1T** operator&() { ATLASSERT(p==NULL); return &p; }
Note that this operator asserts when you take
the address of a non-NULL
smart interface pointer because
you might dereference the returned address and overwrite the
internal member variable without properly releasing the interface
pointer. It asserts to protect the semantics of the pointer and
keep you from accidentally stomping on the pointer. This behavior,
however, keeps you from using a smart interface pointer as an
[in,out]
function parameter.
1STDMETHODIMP SomeClass::UpdateObject (
2 /* [in, out] */ IExpected** ppExpected);
3
4CComPtr<IExpected> pE = /* Initialize to some value */ ;
5
6pobj->UpdateObject (&pE); // Asserts in debug build because
7 // pE is non-NULL
When you really want to use a smart pointer in this way, take the address of the member variable:
1pobj->UpdateObject (&pE.p);
CComPtr and CComQIPtr Resource-Management Operations
A smart interface pointer represents a resource,
albeit one that tries to manage itself properly. Sometimes, though,
you want to manage the resource explicitly. For example, you must
release all interface pointers before calling the
CoUninitialize
method. This means that you can’t wait for
the destructor of a CComPtr
object to release the
interface pointer when you allocate the object as a global or
static variableor even a local variable in main()
. The
destructor for global and static variables executes only after the
main function exits, long after CoUninitialize
runs.
You can release the internal interface pointer
by assigning NULL
to the smart pointer. Alternatively and
more explicitly, you can call the Release
method.
1int main( ) {
2 HRESULT hr = CoInitialize( NULL );
3 If (FAILED(hr)) return 1; // Something is seriously wrong
4
5 CComPtr<IUnknown> punk = /* Initialize to some object */ ;
6 ...
7 punk.Release( ); // Must Release before CoUninitialize!
8
9 CoUninitialize( );
10}
Note that the previous code calls the smart pointer
object’s CComPtr<T>::Release
method because it uses
the dot operator to reference the object. It does not directly call
the underlying interface pointer’s IUnknown::Release
method, as you might expect. The smart pointer’s
CComPtrBase<T>::Release
method calls the underlying
interface pointer’s IUnknown::Release
method and sets the
internal interface pointer to NULL
. This prevents the
destructor from releasing the interface again. Here is the smart
pointer’s Release
method:
1void Release() {
2 T* pTemp = p;
3 if (pTemp) {
4 p = NULL;
5 pTemp->Release();
6 }
7}
It’s not immediately obvious why the
CComPtrBase<T>::Release
method doesn’t simply call
IUnknown::Release
using its p
member variable.
Instead, it copies the interface pointer member variable into the
local variable, sets the member variable to NULL
, and then
releases the interface using the temporary variable. This approach
avoids a situation in which the interface the smart pointer holds
is released twice.
For example, assume that the smart pointer is a
member variable of class A and that the smart pointer holds a
reference to object B. You call the smart pointer’s
.Release
method. The smart pointer releases its reference
to object B. Object B, in turn, holds a reference to the class A
instance containing the smart pointer. Object B decides to release
its reference to the class A instance. The class A instance decides
to destruct, which invokes the destructor for the smart pointer
member variable. The destructor detects that the interface pointer
is non-NULL, so it releases the interface again. [3]
Thanks go to Jim Springfield for pointing this out.
In releases of ATL earlier than version 3, the following code would compile successfully and would release the interface pointer twice. Note the use of the arrow operator.
1punk->Release( ); // Wrong! Wrong! Wrong!
In those releases of ATL, the arrow operator
returned the underlying interface pointer. Therefore, the previous
line actually called the IUnknown::Release
function, not
the CComPtr<T>::Release
method, as expected. This
left the smart pointer’s interface pointer member variable
non-NULL
, so the destructor would eventually release the
interface a second time.
This was a nasty bug to find. A smart pointer class encourages you to think about an instance as if it were an interface pointer. However, in this particular case, you shouldn’t use the arrow operator (which you would if it actually was a pointer); you had to use the dot operator because it was actually an object. What’s worse, the compiler didn’t tell you when you got it wrong.
This changed in version 3 of ATL. Note that the
current definition of the arrow operator returns a
_NoAddRefReleaseOnCComPtr<T>*
value:
1_NoAddRefReleaseOnCComPtr<T>* operator->() const {
2 ATLASSERT(p!=NULL); return (_NoAddRefReleaseOnCComPtr<T>*)p;
3}
This is a simple template class whose only
purpose is to make the AddRef
and Release
methods
inaccessible:
1template <class T>
2class _NoAddRefReleaseOnCComPtr : public T {
3 private:
4 STDMETHOD_(ULONG, AddRef)()=0;
5 STDMETHOD_(ULONG, Release)()=0;
6};
The _NoAddRefReleaseOnCComPtr<T>
template class derives from the interface being returned.
Therefore, it inherits all the methods of the interface. The class
then overrides the AddRef
and Release
methods,
making them private and purely virtual. Now you get the following
compiler error when you use the arrow operator to call either of
these methods:
1error C2248: 'Release' : cannot access private member declared
2 in class 'ATL::_NoAddRefReleaseOnCComPtr<T>'
The CopyTo Method
The
CopyTo
method makes an AddRef
’ed copy of the
interface pointer and places it in the specified location.
Therefore, the CopyTo
method produces an interface pointer
that has a lifetime that is separate from the lifetime of the smart
pointer that it copies.
1HRESULT CopyTo(T** ppT) {
2 ATLASSERT(ppT != NULL);
3 if (ppT == NULL) return E_POINTER;
4 *ppT = p;
5 if (p) p->AddRef();
6 return S_OK;
7}
Often, you use the CopyTo
method to
copy a smart pointer to an [out]
parameter. An
[out]
interface pointer must be AddRef
’ed by the
code returning the pointer:
1STDMETHODIMP SomeClass::get_Object(
2/* [out] */ IExpected** ppExpected) {
3 // Interface saved in member m_object
4 // of type CComPtr<IExpected>
5
6 // Correctly AddRefs pointer
7 return m_object.CopyTo (ppExpected) ;
8}
Watch out for the following codeit probably doesn’t do what you expect, and it isn’t correct:
1STDMETHODIMP SomeClass::get_Object (
2/* [out] */ IExpected** ppExpected) {
3 // Interface saved in member m_object
4 // of type CComPtr<IExpected>
5 *ppExpected = m_object ; // Wrong! Does not AddRef pointer!
6}
The Type-Cast Operator
When you assign a smart pointer to a raw
pointer, you implicitly invoke the operator T()
method. In
other words, you cast the smart pointer to its underlying type.
Notice that operator T()
doesn’t AddRef
the
pointer it returns:
1operator T*() const { return (T*) p; }
That’s because you don’t want the AddRef
in the following case:
1STDMETHODIMP SomeClass::put_Object (
2 /* [in] */ IExpected* pExpected);
3
4// Interface saved in member m_object of type CComPtr<IExpected>
5// Correctly does not AddRef pointer!
6pObj->put_Object (m_object) ;
The Detach and Attach Methods
When you want to transfer ownership of the
interface pointer in a CComPtr
instance from the instance
to an equivalent raw pointer, use the Detach
method. It
returns the underlying interface pointer and sets the smart pointer
to NULL
, ensuring that the destructor doesn’t release the
interface. The client calling Detach
becomes responsible
for releasing the interface.
1T* Detach() { T* pt = p; p = NULL; return pt; }
You often use Detach
when you need to
return to a caller an interface pointer that you no longer need.
Instead of providing the caller an AddRef
’ed copy of the
interface and then immediately releasing your held interface
pointer, you can simply transfer the reference to the caller, thus
avoiding extraneous AddRef
/ Release
calls. Yes,
it’s a minor optimization, but it’s also simple:
1STDMETHODIMP SomeClass::get_Object (
2/* [out] */ IExpected** ppExpected) {
3 CComPtr<IExpected> pobj = /* Initialize the smart pointer */ ;
4 *ppExpected = pobj->Detach(); // Destructor no longer Releases
5 return S_OK;
6}
When you want to transfer ownership of a raw
interface pointer to a smart pointer, use the Attach
method. It releases the interface pointer that the smart pointer
holds and then sets the smart pointer to use the raw pointer. Note
that, again, this technique avoids extraneous
AddRef
/ Release
calls and is a useful minor
optimization:
1void Attach(T* p2) { if (p) p->Release(); p = p2; }
Client code can use the Attach
method to
assume ownership of a raw interface pointer that it receives as an
[out]
parameter. The function that provides an
[out]
parameter is transferring ownership of the interface
pointer to the caller.
1STDMETHODIMP SomeClass::get_Object (
2 /* [out] */ IExpected** ppObject);
3
4void VerboseGetOption () {
5 IExpected* p;
6 pObj->get_Object (&p) ;
7
8 CComPtr<IExpected> pE;
9 pE.Attach (p); // Destructor now releases the interface pointer
10 // Let the exceptions fall where they may now!!!
11 CallSomeFunctionWhichThrowsExceptions();
12}
Miscellaneous Smart Pointer Methods
The smart pointer classes also provide useful
shorthand syntax for querying for a new interface: the
QueryInterface
method. It takes one parameter: the address
of a variable that is of the type of the desired interface.
1template <class Q>
2HRESULT QueryInterface(Q** pp) const {
3 ATLASSERT(pp != NULL && *pp == NULL);
4 return p->QueryInterface(__uuidof(Q), (void**)pp);
5}
This method reduces the chance of making the
common mistake of querying for one interface (for example,
IID_IBar
), but specifying a different type of pointer for
the returned value (for example, IFoo*
).
1CComPtr<IFoo> pfoo = /* Initialize to some IFoo */
2IBar* pbar;
3
4// We specify an IBar variable so the method queries for IID_IBar
5HRESULT hr = pfoo.QueryInterface(&pBar);
Use the IsEqualObject
method to
determine whether two interface pointers refer to the same
object:
1bool IsEqualObject(IUnknown* pOther);
This method performs the test for COM identity:
Query each interface for IID_IUnknown
and compare the
results. A COM object must always return the same pointer value
when asked for its IUnknown
interface. The
IsEqualObject
method expands a little on the COM identity
test. It considers two NULL
interface pointers to be equal
objects.
1bool SameObjects(IUnknown* punk1, IUnknown* punk2) {
2 CComPtr<IUnknown> p (punk1);
3 return p.IsEqualObject (punk2);
4}
5
6IUnknown* punk1 = NULL;
7IUnknown* punk2 = NULL;
8ATLASSERT (SameObjects(punk1, punk2); // true
The SetSite
method associates a site
object (specified by the punkParent
parameter) with the
object referenced by the internal pointer. The smart pointer must
point to an object that implements the IObjectWithSite
interface.
1HRESULT SetSite(IUnknown* punkParent);
The Advise
method associates a
connection point sink object with the object the smart interface
pointer references (which is the event source object). The first
parameter is the sink interface. You specify the sink interface ID
as the second parameter. The third parameter is an output
parameter. The Advise
method returns a token through this
parameter that uniquely identifies this connection.
1HRESULT Advise(IUnknown* pUnk, const IID& iid, LPDWORD pdw);
1CComPtr<ISource> ps /* Initialized via some mechanism */ ;
2ISomeSink* psink = /* Initialized via some mechanism */ ;
3DWORD dwCookie;
4
5ps->Advise (psink, __uuidof(ISomeSink), &dwCookie);
There is no Unadvise
smart pointer
method to end the connection because the pointer is not needed for
the Unadvise
. To break the connection, you need only the
cookie, the sink interface identifier (IID), and an event source
reference.
CComPtr Comparison Operators
Three operators provide comparison operations on
a smart pointer. The operator!()
method returns
true
when the interface pointer is NULL
. The
operator==()
method returns
true
when the comparison operand is equal to the interface
pointer. The operator<()
method is rather useless
because it compares two interface pointers using their binary
values. However, a class needs these comparison operators so that
STL collections of class instances work properly.
1bool operator!() const { return (p == NULL); }
2bool operator< (T* pT) const { return p < pT; }
3bool operator==(T* pT) const { return p == pT; }
4bool operator!=(T* pT) const { return !operator==(pT); }
Using these comparison operators, all the following styles of code work:
1CComPtr<IFoo> pFoo;
2// Tests for pFoo.p == NULL using operator!
3if (!pFoo) {...}
4// Tests for pFoo.p == NULL using operator==
5if (pFoo == NULL) {...}
6// Converts pFoo to T*, then compares to NULL
7if (NULL == pFoo) {...}
The CComPtr Specialization for IDispatch
It’s a royal pain to call an object’s methods
and properties using the IDispatch::Invoke
method. You
have to package all the arguments into VARIANT
structures,
build an array of those VARIANT
s, and translate the name
of the method to a DISPID
. It’s not only extremely
difficult, but it’s all tedious and error-prone coding. Here’s an
example of what it takes to make a simple call to the following
ICalc::Add
method:
1// component IDL file
2[
3 object,
4 uuid(2F6C88D7-C2BF-4933-81FA-3FBAFC3FC34B),
5 dual,
6]
7interface ICalc : IDispatch {
8 [id(1)] HRESULT Add([in] DOUBLE Op1,
9 [in] DOUBLE Op2, [out,retval] DOUBLE* Result);
10};
11
12// client.cpp
13HRESULT CallAdd(IDispatch* pdisp) {
14 // Get the DISPID
15 LPOLESTR pszMethod = OLESTR("Add");
16 DISPID dispid;
17 hr = pdisp->GetIDsOfNames(IID_NULL,
18 &pszMethod,
19 1,
20 LOCALE_SYSTEM_DEFAULT,
21 &dispid);
22
23 if (FAILED(hr))
24 return hr;
25
26 // Set up the parameters
27 DISPPARAMS dispparms;
28 memset(&dispparms, 0, sizeof(DISPPARAMS));
29 dispparms.cArgs = 2;
30
31 // Parameters are passed right to left
32 VARIANTARG rgvarg[2];
33 rgvarg[0].vt = VT_R8;
34 rgvarg[0].dblVal = 6;
35 rgvarg[1].vt = VT_R8;
36 rgvarg[1].dblVal = 7;
37
38 dispparms.rgvarg = &rgvarg[0];
39
40 // Set up variable to hold method return value
41 VARIANTARG vaResult;
42 ::VariantInit(&vaResult);
43
44 // Invoke the method
45 hr = pdisp->Invoke(dispid,
46 IID_NULL,
47 LOCALE_SYSTEM_DEFAULT,
48 DISPATCH_METHOD,
49 &dispparms,
50 &vaResult,
51 NULL,
52 NULL);
53
54 // vaResult now holds sum of 6 and 7
55}
Ouch! That’s pretty painful for such a simple
method call. The code to call a property on an IDispatch
interface is very similar. Fortunately, ATL provides relief from
writing code like this.
CComPtr
provides a specialization for
dealing with the IDispatch
interface:
1//specialization for IDispatch
2template <>
3class CComPtr<IDispatch> : public CComPtrBase<IDispatch> {
4public:
5 CComPtr() {}
6 CComPtr(IDispatch* lp) :
7 CComPtrBase<IDispatch>(lp) {}
8 CComPtr(const CComPtr<IDispatch>& lp) :
9 CComPtrBase<IDispatch>(lp.p) {}
10};
Because this class derives from
CComPtrBase
, it inherits the typical smart pointer
methods. I examine only the ones that differ significantly from
those discussed for the CComPtr
and CComQIPtr
classes.
Property Accessor and Mutator Methods
A few of the methods make it much easier to get
and set properties on an object using the object’s
IDispatch
interface. First, you can get the
DISPID
for a property, given its string name, by calling
the GetIDOfName
method:
1HRESULT GetIDOfName(LPCOLESTR lpsz, DISPID* pdispid);
When you have the DISPID
for a
property, you can get and set the property’s value using the
GetProperty
and PutProperty
methods. You specify
the DISPID
of the property to get or set and send or
receive the new value in a VARIANT
structure:
1HRESULT GetProperty(DISPID dwDispID, VARIANT* pVar);
2HRESULT PutProperty(DISPID dwDispID, VARIANT* pVar);
You can skip the initial step and get and set a
property given only its name using the well-named
GetPropertyByName
and PutPropertyByName
methods:
1HRESULT GetPropertyByName(LPCOLESTR lpsz, VARIANT* pVar);
2HRESULT PutPropertyByName(LPCOLESTR lpsz, VARIANT* pVar);
Method Invocation Helper Functions
The
CComPtr<IDispatch>
specialization has a number of
methods that are customized for the frequent cases of calling an
object’s method(s) using IDispatch
. Four basic variations
exist:
Call a method by
DISPID
or name, passing zero parameters.Call a method by
DISPID
or name, passing one parameter.Call a method by
DISPID
or name, passing two parameters.Call a method by
DISPID
or name, passing an array of N parameters.
Each variation expects the DISPID
or
name of the method to invoke, the arguments, and an optional return
value.
1HRESULT Invoke0(DISPID dispid, VARIANT* pvarRet = NULL);
2HRESULT Invoke0(LPCOLESTR lpszName, VARIANT* pvarRet = NULL);
3HRESULT Invoke1(DISPID dispid, VARIANT* pvarParam1,
4 VARIANT* pvarRet = NULL);
5HRESULT Invoke1(LPCOLESTR lpszName,
6 VARIANT* pvarParam1, VARIANT* pvarRet = NULL);
7HRESULT Invoke2(DISPID dispid,
8 VARIANT* pvarParam1, VARIANT* pvarParam2,
9 VARIANT* pvarRet = NULL);
10HRESULT Invoke2(LPCOLESTR lpszName,
11 VARIANT* pvarParam1, VARIANT* pvarParam2,
12 VARIANT* pvarRet = NULL);
13HRESULT InvokeN(DISPID dispid,
14 VARIANT* pvarParams, int nParams,
15 VARIANT* pvarRet = NULL);
16HRESULT InvokeN(LPCOLESTR lpszName,
17 VARIANT* pvarParams, int nParams,
18 VARIANT* pvarRet = NULL);
Note that when you are creating the parameter arrays, the parameters must be in reverse order: The last parameter should be at element 0, the next-to-last at element 1, and so on.
Using these helper functions, calling the
Add
method gets much simpler:
1HRESULT TheEasyWay( IDispatch *spCalcDisp ) {
2 CComPtr< IDispatch > spCalcDisp( pCalcDisp );
3
4 CComVariant varOp1( 6.0 );
5 CComVariant varOp2( 7.0 );
6 CComVariant varResult;
7 HRESULT hr = spCalcDisp.Invoke2( OLESTR( "Add" ),
8 &varOp1, &varOp2, &varResult );
9 // varResult now holds sum of 6 and 7
10}
Finally, two static member
functions exist: GetProperty
and SetProperty
. You
can use these methods to get and set a property using its
DISPID
, even if you haven’t encapsulated the
IDispatch
pointer in a
CComPtr<IDispatch>
.
1static HRESULT GetProperty(IDispatch* pDisp, DISPID dwDispID,
2 VARIANT* pVar);
3static HRESULT PutProperty(IDispatch* pDisp, DISPID dwDispID,
4 VARIANT* pVar);
Here’s an example:
1HRESULT GetCount(IDispatch* pdisp, long* pCount) {
2 *pCount = 0;
3 const int DISPID_COUNT = 1;
4
5 CComVariant v;
6 CComPtr<IDispatch>::GetProperty (pdisp, DISPID_COUNT, &v);
7
8 HRESULT hr = v.ChangeType (VT_I4);
9 If (SUCCEEDED (hr))
10 *pCount = V_I4(&v) ;
11 return hr;
12}
The CComGITPtr Class
The Global Interface Table (GIT) provides a per-process cache for storing COM interfaces that you can efficiently unmarshal and access from any apartment in a process. COM objects that aggregate the free-threaded marshaler typically use the GIT to unmarshal interfaces that they hold as state because the object never knows which apartment it might be called from. The GIT provides a convenient place where objects that export an interface from their apartment can register interfaces, and where objects that import interfaces into their apartment can unmarshal and use the interface.
Typically, several steps are involved in using
the GIT. First, the exporting apartment must use
CoCreateInstance
to create an instance of the GIT and
obtain an IGlobalInterfaceTable
pointer. The exporting
apartment then calls
IGlobalInterfaceTable::RegisterInterfaceInGlobal
to
register the interface in the GIT. As a result of the call to
RegisterInterfaceInGlobal
, the exporting apartment
receives an apartment-neutral cookie that can safely be passed to
other apartments (but not other
processes) for unmarshaling. Any number of objects in any importing
apartment can then use this cookie to retrieve an interface
reference that is properly unmarshaled for use in their own
apartment.
The code in the exporting apartment might typically look like the following:
1HRESULT RegisterMyInterface(IMyInterface* pmi, DWORD* pdwCookie) {
2 // this is usually a global
3 IGlobalInterfaceTable* g_pGIT = NULL;
4 HRESULT hr = ::CoCreateInstance(CLSID_StdGlobalInterfaceTable,
5 NULL,
6 CLSCTX_INPROC_SERVER,
7 IID_IGlobalInterfaceTable,
8 (void**)&g_pGIT);
9ATLASSERT(SUCCEEDED(hr));
10hr = g_pGIT->RegisterInterfaceInGlobal(pmi,
11 __uuidof(pmi), pdwCookie);
12return hr;
13}
The pdwCookie
returned to the exporting
apartment then is passed to another apartment. By using that
cookie, any code in that apartment can retrieve the interface
pointer registered in the GIT.
1HRESULT ReadMyInterface(DWORD dwCookie) {
2 // ... GIT pointer obtained elsewhere
3 IMyInterface* pmi = NULL;
4 hr = g_pGIT->GetInterfaceFromGlobal(dwCookie,
5 __uuidof(pmi), (void**)&pmi);
6 // use pmi as usual
7 return hr;
8}
The exporting apartment removes the interface
from the GIT by calling
IGlobalInterfaceTable::RevokeInterfaceFromGlobal
and
passing in the cookie it originally received.
ATL simplifies the coding required to perform
the following steps by encapsulating the GIT functions in the
CComGITPtr
smart pointer class. This class is defined in
atlbase.h
as follows:
1template <class T>
2class CComGITPtr
3{
4 // ...
5 DWORD m_dwCookie;
6};
This class accepts an interface type as its
template parameter and holds as its only state the cookie for the
interface that will be registered in the GIT. The previously
described operations that the exporting apartment performed are
encapsulated by CComGITPtr
. Under the covers,
CComGITPtr
simply manipulates the same GIT functions that
would otherwise be invoked manually. Even the creation and caching
of the GIT itself is managed for you. CComGITPtr
retrieves
a reference to the GIT from CAtlModule
, which instantiates
the GIT automatically the first time it is accessed and caches the
resulting interface pointer for subsequent accesses. This class
holds all sorts of information that is global to a COM server; this
is discussed in detail in Chapter 5, “COM Servers.”
CComGITPtr
instances can be
instantiated with four different constructors:
1CComGITPtr() ;
2CComGITPtr(T* p);
3CComGITPtr(const CComGITPtr& git);
4explicit CComGITPtr(DWORD dwCookie) ;
The first constructor simply initializes the
m_dwCookie
member variable to zero. The second constructor
accepts an interface pointer. This constructor retrieves an
IGlobalInterfaceTable
pointer to a global instance of the
GIT and calls RegisterInterfaceInGlobal
. The resulting
cookie is cached in m_dwCookie
. The third constructor
accepts a reference to an existing instance of CComGITPtr
.
This overload retrieves the interface associated with the passed-in
git
parameter, reregisters it in the GIT to get a second
cookie, and stores the new cookie in its own m_dwCookie
member variable. This leaves the two CComGITPtr
s with
separate registered copies of the same interface pointer. The
fourth constructor accepts a cookie directly and caches the value.
One nice thing about this constructor is that, in debug builds, the
implementation tries to validate the cookie by retrieving an
interface from the GIT using the cookie. The constructor asserts if
this fails.
The three assignment operators
CComGITPtr
supplies perform operations identical to those
of the corresponding constructors:
1CComGITPtr<T>& operator=(T* p)
2CComGITPtr<T>& operator=(const CComGITPtr<T>& git)
3CComGITPtr<T>& operator=(DWORD dwCookie)
What’s particularly nice about
CComGITPtr
is that the destructor takes care of the
required GIT cleanup when the instance goes out of scope. Beware,
thoughthis can get you into a bit of trouble if you’re not careful,
as you’ll learn at the end of this section.
1~CComGITPtr() { Revoke(); }
As you can see, the
destructor simply delegates its work to the Revoke
method,
which takes care of retrieving an IGlobalInterfaceTable
pointer and using it to call
RevokeInterfaceFromGlobal
.
1HRESULT Revoke() {
2 HRESULT hr = S_OK;
3 if (m_dwCookie != 0) {
4 CComPtr<IGlobalInterfaceTable> spGIT;
5 HRESULT hr = E_FAIL;
6 hr = AtlGetGITPtr(&spGIT);
7
8 ATLASSERT(spGIT != NULL);
9 ATLASSERT(SUCCEEDED(hr));
10 if (FAILED(hr))
11 return hr;
12
13 hr = spGIT->RevokeInterfaceFromGlobal(m_dwCookie);
14 if (SUCCEEDED(hr))
15 m_dwCookie = 0;
16 }
17 return hr;
18}
If you are working with a CComGITPtr
instance that has already been initialized, you can use one of the
Attach
methods to associate a different interface with the
instance:
1HRESULT Attach(T* p) ;
2HRESULT Attach(DWORD dwCookie) ;
The first version of Attach
calls
RevokeInterfaceFromGlobal
if m_dwCookie
is
nonzero – that is, if this CComGITPtr
is already managing an
interface registered in the GIT. It then calls
RegisterInterfaceInGlobal
using the new interface
p
passed in and stores the resulting cookie. The second
overload also removes the interface it managed from the GIT (if
necessary) and then simply caches the cookie provided.
Correspondingly, the Detach
method can
be used to disassociate the interface from the CComGITPtr
instance.
1DWORD Detach() ;
This method simply returns the stored cookie
value and sets m_dwCookie
to zero. This means that the
caller has now taken ownership of the registered interface pointer
and must eventually call RevokeInterfaceFromGlobal
.
These methods greatly simplify the code needed to register an interface pointer in the GIT and manage that registration. In fact, the code required in the exporting apartment reduces to a single line.
1HRESULT RegisterMyInterface(IMyInterface* pmi) {
2 CComGITPtr<IMyInterface> git(pmi);
3 // creates GIT or gets ref to existing GIT
4 // registers interface in GIT
5 // retrieves cookie and caches it
6
7 // ... interface removed from GIT when git goes out of scope
8}
In the importing apartment, clients that want to
use the registered interface pointer simply use the CopyTo
method CComGITPtr
provides:
1HRESULT CopyTo(T** pp) const
This can be used in code like this:
1HRESULT ReadMyInterface(const CComGITPtr<IMyInterface>& git) {
2 IMyInterface* pmi = NULL;
3 HRESULT hr = git.CopyTo(&pmi);
4 ATLASSERT(SUCCEEDED(hr));
5
6 //... use pmi as usual
7}
A potentially dangerous race condition occurs if
you’re not careful using CComGITPtr
. Remember that the
entire reason for having a GIT is to make an interface accessible
from multiple threads. This means that you will be passing GIT
cookies from an exporting apartment that is not synchronized with
code in the importing apartment that will be using the associated
registered interface. If the lifetime of the CComGITPtr
is
not carefully managed, the importing apartment could easily end up
with an invalid cookie. Here’s the scenario:
1void ThreadProc(void*); // forward declaration
2HRESULT RegisterInterfaceAndFork(IMyInterface* pmi) {
3 CComGITPtr<IMyInterface> git(pmi); // interface registered
4 // create worker thread and pass CComGITPtr instance
5 ::_beginthread(ThreadProc, 0, &git);
6}
7void ThreadProc(void* pv)
8{
9 CComGITPtr<IMyInterface>* pgit =
10 (CComGITPtr<IMyInterface>*)pv;
11 IMyInterface* pmi = NULL;
12 HRESULT hr = pgit->CopyTo(&pmi);
13 // ... do some work with pmi
14}
The trouble with this code
is that the RegisterInterfaceAndFork
method could finish
before the THReadProc
retrieves the interface pointer
using CopyTo
. This means that the git
variable
will go out of scope and unregister the IMyInterface
pointer from the GIT too early. You must employ some manner of
synchronization, such as WaitForSingleObject
, to guard
against problems like these.
In general, CComGITPtr
shouldn’t be
used as a local variable. Its intended use is as a member variable
or global. In those cases, the lifetime of the CComGITPtr
object is automatically controlled by the lifetime of the object
that contains it, or the lifetime of the process.
The CAutoPtr and CAutoVectorPtr Smart Pointer Classes
CComPtr
was presented as a smart
pointer class for managing a COM interface pointer. ATL 8 provides
a related set of classes for managing pointers to instances of C++
classes, as opposed to CComPtr
’s management of interface
pointers to COM coclasses. These classes provide a useful
encapsulation of the operations required to properly manage the
memory resources associated with a C++ object. CAutoPtr
,
CAutoVectorPtr
, CAutoPtrArray
, and
CAutoPtrList
are all defined in atlbase.h
.
The CAutoPtr and CAutoVectorPtr Classes
The CAutoPtr
template class wraps a C++
object created with the new operator. The class holds a pointer to
the encapsulated object as its only state, and exposes convenient
methods and operators for controlling the ownership, lifetime, and
state of the internal C++ object. CAutoPtr
is used in code
like this:
1STDMETHODIMP CMyClass::SomeFunc() {
2 CFoo* pFoo = new Foo(); // instantiate C++ class
3 CAutoPtr<CFoo> spFoo(pFoo); // take ownership of pFoo
4 spFoo->DoSomeFoo();
5 // ... do other things with spFoo
6} // CAutoPtr deletes pFoo instance
7 // when spFoo goes out of scope
This simple example demonstrates the basic usage
pattern of CAutoPtr
: Create an instance of a C++ class,
transfer ownership of the pointer to CAutoPtr
, operate on
the CAutoPtr
object as if it were the original C++ class,
and let CAutoPtr
destroy the encapsulated object or
reclaim ownership of the pointer. Although this behavior is similar
to that of the Standard C++ class auto_ptr
, that class
throws exceptions, whereas ATL’s CAutoPtr
does not. ATL
developers sometimes do not link with the CRT, so the exception
support required by auto_ptr
would not be available.
CAutoVectorPtr
enables you to manage a
pointer to an array of C++ objects. It operates almost identically
to CAutoPtr
; the principal difference is that vector
new[]
and vector delete[]
are used to allocate
and free memory for the encapsulated objects. The comments in the
sections that follow are written in terms of CAutoPtr
,
although most apply equally well to both CAutoPtr
and
CAutoVectorPtr
.
Constructors and Destructor
CAutoPtr
provides four constructors to
initialize new instances. The first constructor simply creates a
CAutoPtr
instance with a NULL
-encapsulated
pointer.
1CAutoPtr() : m_p( NULL ) { }
2
3template< typename TSrc >
4CAutoPtr( CAutoPtr< TSrc >& p ) {
5 m_p = p.Detach(); // Transfer ownership
6}
7
8CAutoPtr( CAutoPtr< T >& p ) {
9 m_p = p.Detach(); // Transfer ownership
10}
11
12explicit CAutoPtr( T* p ) : m_p( p ) { }
To use this class to do any meaningful work, you
have to associate a pointer with the instance using either the
Attach
method or one of the assignment operators (these
are discussed shortly). The third constructor enables you to
initialize with another CAutoPtr
instance. This simply
uses the Detach
method to transfer ownership of the object
encapsulated from the CAutoPtr
instance passed in to the
instance being constructed. The fourth constructor also transfers
ownership of an object pointer, but using the object pointer
directly as the constructor parameter. The second constructor is an
interesting one. It defines a templatized constructor with a second type
parameter, TSrc
. This second template parameter represents
a second type from which the CAutoPtr
instance can be
initialized. The type of the encapsulated pointer within
CAutoPtr
instance was established at declaration by the
CAutoPtr
class’s template parameter T
, as in this
declaration:
1CAutoPtr<CAnimal> spAnimal(pAnimal);
Here, CAnimal
is the declared type of
the encapsulate m_p
object pointer. So, how is it that we
have a constructor that enables us to initialize this pointer to a
pointer of a different type? The answer is quite simple. Just as
C++ allows pointers to instances of base types to be initialized
with pointers to instances of derived types, the fourth
CAutoPtr
constructor allows instances of
CAutoPtr<Base>
to be initialized with instances of
CAutoPtr<Derived>
, as in the following:
1class CAnimal { ... };
2class CDog : public CAnimal { ... };
3//
4...
5CDog* pDog = new CDog();
6CAutoPtr<CAnimal> spAnimal(pDog);
The CAutoPtr
destructor is invoked
whenever the instance goes out of scope. It leverages the
Free
method to release the memory associated with the
internal C++ class object.
1~CAutoPtr() {
2 Free();
3}
4void Free() {
5 delete m_p;
6 m_p = NULL;
7}
CAutoPtr Operators
CAutoPtr
defines two assignment
operators:
1template< typename TSrc >
2CAutoPtr< T >& operator=( CAutoPtr< TSrc >& p ) {
3 if(m_p==p.m_p) {
4 ATLASSERT(FALSE);
5 } else {
6 Free();
7 Attach( p.Detach() ); // Transfer ownership
8 }
9 return( *this );
10}
11
12CAutoPtr< T >& operator=( CAutoPtr< T >& p ) {
13 if(*this==p) {
14 if(this!=&p) {
15 ATLASSERT(FALSE);
16 p.Detach();
17 } else {
18 }
19 } else {
20 Free();
21 Attach( p.Detach() ); // Transfer ownership
22 }
23 return( *this );
24}
Both of these operators behave the same. The
difference is that the first version is templatized to a second
type TSrc
. As with the templatized constructor just
discussed that accepts a TSrc
template parameter, this
assignment operator allows for assignment of pointers to instances
of base types to be assigned to pointers to instances of derived
types. You can take advantage of this flexibility in code such as
the following:
1class CAnimal { ... };
2class CDog : public CAnimal { ... };
3// ...
4
5// instantiate a CAnimal
6CAutoPtr<CAnimal> spAnimal(new CAnimal());
7
8// instantiate a CDog
9CAutoPtr<CDog> spDog(new CDog());
10// CAnimal instance freed here
11spAnimal = spDog;
12
13// ... CDog instance will be freed when spAnimal
14// goes out of scope
Regardless of whether you assign a
CAutoPtr
instance to the same type or to a derived type,
the assignment operator first checks for some important misuses
(such as multiple CAutoPtr
objects pointing to the same
underlying C++ object) and then calls the Free
method to
delete the encapsulated instance before taking ownership of the new
instance. The call to p.Detach
ensures that the instance
on the right side of the assignment does not also try to delete the
same object.
CAutoPtr
also defines a cast operator
and overloads the member access operator (->
):
1operator T*() const {
2 return( m_p );
3}
4T* operator->() const {
5 ATLASSERT( m_p != NULL );
6 return( m_p );
7}
Both operators simply return the value of the
encapsulated pointer. The member access operator exposes the public
member functions and variables of the encapsulated object, so you
can use instances of CAutoPtr
more like the encapsulated
type:
1class CDog {
2public:
3 void Bark() {}
4 int m_nAge;
5};
6CAutoPtr<CDog> spDog(new Dog);
7spDog->Bark();
8spDog->m_nAge += 5;
Finally, CAutoPtr defines operator==
and operator!=
to do comparisons between two CAutoPtr
objects.
1bool operator!=(CAutoPtr<T>& p) const { return !operator==(p); }
2bool operator==(CAutoPtr<T>& p) const { return m_p==p.m_p; }
CAutoVectorPtr
CAutoVectorPtr
differs from
CAutoPtr
in only a few ways. First,
CAutoVectorPtr
does not define a constructor that allows
the initialization of an instance using a derived type. Second,
CAutoVectorPtr
defines an Allocate
function to
facilitate construction of a collection of encapsulated
instances.
1bool Allocate( size_t nElements ) {
2 ATLASSERT( m_p == NULL );
3 ATLTRY( m_p = new T[nElements] );
4 if( m_p == NULL ) {
5 return( false );
6 }
7
8 return( true );
9}
This method simply uses the vector
new[]
to allocate and initialize the number of instances
specified with the nElements
parameter. Here’s how you
might apply this capability:
1class CAnimal { public: void Growl() {} };
2// each instance is of type CAnimal
3CAutoVectorPtr<CAnimal> spZoo;
4// allocate and initialize 100 CAnimal's
5spZoo.Allocate(100);
Note that CAutoVectorPtr
does not
overload the member access operator (->
), as did
CAutoPtr
. So, you cannot write code like this:
1spZoo->Growl(); // wrong! can't do this => doesn't make sense
Of course, such an operation doesn’t even make
sense because you’re not specifying which CAnimal
instance
should growl. You can operate on the encapsulated instances only
after retrieving a specific one from the encapsulated collection.
It’s not clear why the ATL team didn’t overload operator[]
to provide a convenient arraylike syntax for accessing individual
instances contained in a CAutoVectorPtr
instance. So, you
have to write code such as the following to get at members of a
particular encapsulated instance:
1((CAnimal*)spZoo)[5].Growl();
I find myself underwhelmed that the ATL team
didn’t simply overload operator[]
to provide a more
convenient arraylike syntax for accessing individual members of the
collection. But, hey, it’s their worldI’m just livin’ in it.
CAutoVectorPtr
has another limitation,
but, unfortunately, I can’t point the finger at Microsoft for this
one. A consequence of CAutoVectorPtr
using the vector
new[]
to allocate the collection of encapsulated objects
is that only the default constructor of the encapsulated type is
invoked. If the class you want CAutoVectorPtr
to manage
defines nondefault constructors and performs special initialization
in them, the Allocate
function has no way to call these
constructors. This also means that your class must define a default
(parameterless) constructor, or you won’t even be able to use your
class with CAutoVectorPtr
. So, if we change our
CAnimal
, this code won’t compile:
1class CAnimal {
2public:
3CAnimal(int nAge) : m_nAge(nAge) {}
4 void Growl() {}
5private:
6int m_nAge;
7}
8
9CAutoVectorPtr<CAnimal> spZoo;
10spZoo.Allocate(100); // won't compile => no default constructor
The final difference CAutoVectorPtr
has
with CAutoPtr
is in the implementation of the destructor.
Allocate
used the vector new[]
to create and
initialize the collection of encapsulated instances, so the
destructor must match this with a vector delete[]
operation. In C++, you must always match the vector allocation
functions this way; otherwise, bad things happen. This ensures that
the destructor for each object in the collection is run and that
all the associated memory for the entire collection is properly
released. Exactly what happens if this regimen isn’t followed is
compiler specific (and also compiler setting specific). Some
implementations corrupt the heap immediately if delete[]
is not used; others invoke the destructor of only the first object
in the collection. CAutoVectorPtr
does the right thing for
you if you have let it handle the allocation via the
Allocate
member function. However, you can get yourself
into trouble by improperly using Attach
, with code such as
the following:
1class CAnimal {};
2// allocate only a single instance
3CAnimal* pAnimal = new Animal;
4CAutoVectorPtr<CAnimal> spZoo;
5// wrong, wrong!!! pAnimal is not a collection
6spZoo.Attach(pAnimal)
In this code, the original pointer to the C++
instance was allocated using new
instead of vector
new[]
. So, when spZoo
goes out of scope and the
destructor runs, it will eventually call vector delete[]
.
That will be bad. In fact, it will throw an exception, so be
careful.
ATL Memory Managers
A Review of Windows Memory Management
Applications use memory for almost everything they do. In Windows, memory can be allocated from three principle places: the thread’s stack, memory-mapped files, and heaps. Memory-mapped files are more specialized, so we don’t discuss them further here. The stack is used to allocate local variables because their size is known at compile time and their allocation must be as efficient as possible. Allocating and deallocating storage from the stack involves merely incrementing and decrementing the stack pointer by the appropriate amount. Dynamic memory, however, is allocated and freed as the program runs, based on changing characteristics within the application. Instead of being allocated from the thread’s stack, dynamic memory comes from pools of storage known as heaps. A heap is an independently managed block of memory that services dynamic memory allocation requests and reclaims memory that an application no longer uses. Typically, heaps expose APIs for creating the heap, destroying the heap, allocating a block of memory within the heap, and returning a block of memory to the heap. The precise algorithms employed in coordinating these tasks constitute what is commonly termed the heap manager. In general, heap managers implement various schemes for managing resources for specialized circumstances. The heap manager functions exposed for applications often reflect some of the differences that make one particular type of heap suitable over another for a particular circumstance.
In Windows, each process creates a default heap at initialization. Applications
use Win32 functions such as HeapCreate
,
HeapAlloc
, and HeapFree
to manage the heap and
blocks of data within the default heap. Because many Windows
functions that can be called from multiple applications use this
heap, the default heap is implemented to be thread safe. Access to
the default heap is serialized so that multiple simultaneous
threads accessing the heap will not corrupt it. Older versions of
Windows used functions such as LocalAlloc
and
GlobalAlloc
to manipulate the heap, but these functions
are now deprecated. They run slower and offer fewer features than
the HeapXXX
suite.
Applications that link to the C-runtime library
(which ATL projects now do by default) have access to another heap
simply known as the CRT heap. The memory-management functions the
CRT heap manager exposes are likely the most recognizable to the
general C/C++ community because they are part of the C standard.
With the CRT heap, functions such as malloc
are used to
obtain storage from the heap; free
is used to return
storage to the heap.
An application might need to use different heaps
for various reasons, such as with specialized management
requirements. For instance, COM introduces an additional set of
complexities to the general problem of memory management. Memory
addresses are allocated on a per-process basis, so a process cannot
directly access data stored in another process. Yet COM allows data
to be marshaled between processes. If a method call
is remoted so that a client expects an [out]
parameter
from the object, the memory for that [out]
parameter will
be allocated in one process (the object’s), and used and freed in
another process (the client’s). Clearly, a conventional heap has no
way to straddle process boundaries to associate allocations in one
process with free operations in another. The COM task allocator
lives to provide this very service. Part of the COM programming
conventions is that when allocating memory blocks that will be
shared across a COM interface, that memory must be allocated by
calling CoTaskMemAlloc
and must be freed by the
corresponding CoTaskMemFree
. By agreeing on these
standardized functions, the automatically generated proxy-stub code
can properly allocate and free memory across COM boundaries. COM’s
remoting infrastructure does all the dirty work needed to create
the illusion of a single heap that spans processes.
Several other reasons exist for managing memory with different heaps. Components that are allocated from separate heaps are better isolated from one another, which could make the heaps less susceptible to corruption. If objects will be accessed close together in timesay, within the same functionit is desirable for those objects to live close together in memory, which can result in fewer page faults and a marked improvement in overall performance. Some applications choose to implement custom, specialized memory managers that are tuned to specific requirements. Using separate heaps also could allow the application to avoid the overhead associated with synchronizing access to a single heap. As previously mentioned, a Win32 process’s default heap is thread safe because it expects to be accessed simultaneously from multiple threads. This leads to thread contention because each heap access must pass through thread-safe interlocked operations. Applications can devote one heap to each thread and eliminate the synchronization logic and thread contention.
ATL simplifies the use of heaps through a series of concrete heap implementations that wrap Windows API functions and through an abstract interface that allows these implementations to be used polymorphically in other ATL classes that use memory resources.
The IAtlMemMgr Interface
The atlmem.h
header file expresses the
generic memory-management pattern through the definition of the
IAtlMemMgr
interface.
1__interface IAtlMemMgr {
2public:
3 void* Allocate( size_t nBytes ) ;
4 void Free( void* p ) ;
5 void* Reallocate( void* p, size_t nBytes ) ;
6 size_t GetSize( void* p ) ;
7};
The four simple functions
defined on this interface provide most of the dynamic memory
functionality required in typical applications. Allocate
reserves a contiguous region of space nBytes
in size
within the heap. Free
takes a pointer to a memory block
retrieved from Allocate
and returns it to the heap so that
it will be available for future allocation requests. The
Reallocate
method is useful when an allocated block is not
large enough to accommodate additional data and it is more
practical and/or efficient to grow the existing block than to
allocate a new, larger one and copy the contents. Finally,
GetSize
accepts a pointer to a block obtained from
Allocate
and returns the current size of the block in
bytes.
Many ATL classes are designed to support
pluggable heap implementations by performing all their
memory-management functions through an IAtlMemMgr
reference. Developers can provide custom implementations of
IAtlMemMgr
and use them with ATL. This provides a great
deal of flexibility in optimizing the performance of these classes
to suit specific application requirements. ATL Server makes heavy
use of IAtlMemMgr
in processing SOAP requests and in
stencil processing. Additionally, we’ve already seen how
CStringT
allows developers to supply an
IAtlMemMgr
implementation to optimize string-handling
performance.
The Memory Manager Classes
Although it is useful to abstract memory
management behind an interface to facilitate custom heap
implementations, most applications don’t need a high degree of
sophistication in these implementations to build efficient
components. Indeed, you can realize many of the benefits of
multiple heaps with simple heap implementations. To that end, ATL
provides five concrete implementations of IAtlMemMgr
that
you can use as is in many circumstances.
CComHeap
is defined in
atlcommem.h
as follows:
1class CComHeap :
2 public IAtlMemMgr {
3// IAtlMemMgr
4public:
5 virtual void* Allocate( size_t nBytes ) {
6#ifdef _WIN64
7 if( nBytes > INT_MAX ) { return( NULL ); }
8#endif
9 return( ::CoTaskMemAlloc( ULONG( nBytes ) ) );
10 }
11 virtual void Free( void* p ) {
12 ::CoTaskMemFree( p );
13 }
14 virtual void* Reallocate( void* p, size_t nBytes ) {
15#ifdef _WIN64
16 if( nBytes > INT_MAX ) { return( NULL ); }
17#endif
18 return( ::CoTaskMemRealloc( p, ULONG( nBytes ) ) );
19 }
20 virtual size_t GetSize( void* p ) {
21 CComPtr< IMalloc > pMalloc;
22 ::CoGetMalloc( 1, &pMalloc );
23 return( pMalloc->GetSize( p ) );
24 }
25};
As
you can see, this class is merely a very thin wrapper on top of the
COM task allocator API functions. Allocate
simply
delegates to CoTaskMemAlloc
, and Free
delegates
to CoTaskMemFree
. In fact, all five of the stock memory
managers implement IAtlMemMgr
in a similar manner; the
prime difference is the underlying functions to which the managers
delegate. Table 3.2
summarizes which heap-management functions are used for each of the
ATL memory managers.
Table 3.2. Heap Functions Used in ATL Memory Managers
Memory Manager Class |
Heap Functions Used |
---|---|
|
|
|
|
|
|
|
|
|
|
The CCRTHeap
uses memory from the CRT
heap, whereas CLocalHeap
and CGlobalHeap
both
allocate memory from the process heap. The LocalXXX
and
GlobalXXX
functions in the Win32 API exist now mostly for
backward compatibility. You shouldn’t really use them in new code
anymore, so we don’t discuss them further.
The CWin32Heap
class is a bit different
from the other heap classes in a couple important respects. Whereas
the other memory managers allocate storage from the process heap,
CWin32Heap
requires that a valid HANDLE
to a heap
be created before using its IAtlMemMgr
implementation.
This gives the developer a bit more control over the details of the
underlying heap that will be used, albeit with a bit more
complexity. CWin32Heap
supplies three constructors for
initializing an instance:
1CWin32Heap() : m_hHeap( NULL ), m_bOwnHeap( false ) { }
2CWin32Heap( HANDLE hHeap ) :
3 m_hHeap( hHeap ),
4 m_bOwnHeap( false ) {
5 ATLASSERT( hHeap != NULL );
6}
7CWin32Heap( DWORD dwFlags, size_t nInitialSize,
8 size_t nMaxSize = 0 ) :
9 m_hHeap( NULL ),
10 m_bOwnHeap( true ) {
11 ATLASSERT( !(dwFlags&HEAP_GENERATE_EXCEPTIONS) );
12 m_hHeap = ::HeapCreate( dwFlags, nInitialSize, nMaxSize );
13 if( m_hHeap == NULL ) { AtlThrowLastWin32(); }
14}
The first constructor initializes a
CWin32Heap
instance with no associated heap. The second
constructor initializes the instance with a handle to an existing
heap obtained from a previous call to the Win32 HeapCreate
API. Note that the m_bOwnHeap
member is set to
false
in this case. This member tracks whether the
CWin32Heap
instance owns the underlying heap. Thus, when
the second constructor is used, the caller is still responsible for
ultimately calling HeapDestroy
to get rid of the heap
later. The third constructor is arguably the simplest to use
because it directly accepts the parameters required to create a
heap and invokes HeapCreate
automatically. The
dwFlags
parameter is a bit field that allows two different
flags to be set. One of the two flags,
HEAP_GENERATE_EXCEPTIONS
, can be given to the underlying
HeapCreate
call to indicate that the system should raise
an exception upon function failure. However, the code asserts if
this flag is specified because the ATL code base isn’t prepared for
system exceptions to be thrown if an allocation fails. The other
flag, HEAP_NO_SERIALIZE
, relates to the synchronization
options with heaps, discussed a bit earlier in this section. If
this flag is specified, the heap is not thread safe. This can improve performance
considerably because interlocked operations are no longer used to
gain access to the heap. However, it is
the programmer’s responsibility to ensure that multiple threads
will not access a heap created with this flag set. Otherwise, heap
corruption is likely to occur. The nInitialSize
parameter indicates how much storage should be reserved when the
heap is created. You can use the nMaxSize
parameter to
specify how large the heap should be allowed to grow.
CWin32Heap
also defines Attach
and Detach
operations to associate an existing heap with a
CWin32Heap
instance:
1void Attach( HANDLE hHeap, bool bTakeOwnership ) {
2 ATLASSERT( hHeap != NULL );
3 ATLASSERT( m_hHeap == NULL );
4 m_hHeap = hHeap;
5 m_bOwnHeap = bTakeOwnership;
6}
7HANDLE Detach() {
8 HANDLE hHeap;
9
10 hHeap = m_hHeap;
11 m_hHeap = NULL;
12 m_bOwnHeap = false;
13
14 return( hHeap );
15}
Attach
accepts a handle to a heap and a
Boolean flag indicating whether the caller is transferring
ownership of the heap to the CWin32Heap
instance. This
governs whether the destructor will destroy the heap.
Detach
simply surrenders ownership of the encapsulated
heap by flipping the m_bOwnHeap
member to FALSE
and returning the handle to the caller. Note that Attach
simply overwrites the existing heap handle stored in the
CWin32Heap
. If the class already held a non-NULL HANDLE
, there would be no way to free that heap after the
Attach
is performed. As a result, you have a memory
leakand a really big one, at that. If you thought leaking memory
from an object was bad, trying leaking entire heaps at a time! You
might wonder at first why the Attach
method doesn’t simply
destroy the existing heap before overwriting the internal handle.
After all, CComVariant::Attach
and
CComSafeArray::Attach
were shown earlier clearing their
encapsulated data before attaching to a new instance. The
difference here is that even if the CWin32Heap
instance
owns the heap (m_bOwnHeap
is TRUE
), it has no
knowledge of what live objects out there have been allocated from
that heap. Blindly destroying the existing heap would yank memory
from any number of objects, which could be disastrous. You simply
have to be careful. Here’s the kind of code you want to avoid:
1// create an instance and allocate a heap
2CWin32Heap heap(0, // no exceptions, use thread-safe access
3 4000, // initial size
4 0); // no max size => heap grows as needed
5
6// manually create a second heap
7HANDLE hHeap = ::HeapCreate(0, 5000, 0);
8
9// this is gonna get you in a "heap" of trouble!
10heap.Attach(hHeap, false /* same result if true */ );
Custom memory management commonly is used in string processing. Applications that allocate, free, and resize strings frequently can often tax memory managers and negatively impact performance. Multithreaded applications that do a lot of string processing can exhibit reduced performance because of thread contention for heap allocation requests. Moreover, heaps that service multithreaded applications can provide slower access because synchronization locks of some sort must be employed to ensure thread safety. One tactic to combat this is to provide a per-thread heap so that no synchronization logic is needed and thread contention does not occur.
We show an example of a specialized heap for
string allocations using CWin32Heap
and ATL’s new
CStencil
class. This class is discussed in detail in later
chapters when we cover building web applications with ATL Server.
For now, recall from the discussion of web application development
in Chapter 1, “Hello,
ATL,” that ATL produces web pages by processing stencil response
files and rendering HTML-based text responses. This involves a
great deal of string parsing and processing, and CStencil
bears a lot of this burden. Its constructor enables you to pass in
a custom memory manager to be used in all its string parsing. The
following code demonstrates how to create a per-thread heap manager
to be used for stencil processing:
1DWORD g_dwTlsIndex; // holds thread-local storage slot index
2// g_dwTlsIndex = ::TlsAlloc() performed in other
3// initialization code
4
5// Create a private heap for use on this thread
6// only => no synchronized access
7CWin32Heap* pHeap = new CWin32Heap(HEAP_NO_SERIALIZE, 50000);
8
9// Store the heap pointer in this thread's TLS slot
10::TlsSetValue(g_dwTlsIndex, reinterpret_cast<void*>(
11static_cast<IAtlMemMgr*>(pHeap)));
12
13// ...
14
15// Retrieve the heap pointer from TLS
16pHeap = (IAtlMemMgr*)::TlsGetValue(g_dwTlsIndex);
17
18// Create a new CStencil instance that uses the private heap
19CStencil* pStencil = new CStencil(pHeap);
Notice the extra layer of casting when storing
the heap pointer in the TLS slot. You need to hold on to the
original CWin32Heap
pointer with the concrete type because
IAtlMemMgr
doesn’t have a virtual destructor. If you just
had an IAtlMemMgr*
to call delete on, the actual
CWin32Heap
destructor would not get called. That extra
layer of casting is to make sure that you get the correct interface
pointer converted to void*
before storing it in the TLS.
It’s probably not strictly necessary in the current version of ATL,
but if the heap implementation has multiple base classes, the cast
to void*
could cause some serious trouble.
Summary
ATL provides a rich set of classes for
manipulating the data types COM programmers frequently use. The
CComVariant
class provides practically the same benefits
as the CComBSTR
class, but for VARIANT
structures. If you use a VARIANT
structureand you will
need to use one sooner or lateryou should instead use the ATL
CComVariant
smart VARIANT
class. You’ll have far
fewer problems with resource leaks.
SAFEARRAYs
have a number of specialized
semantics as well. CComSafeArray
was shown to be a useful
template class for managing both single- and multidimensional
SAFEARRAY``s. As with other managed resources, however, take
care when dealing with ``CComSafeArray
because the compiler
cannot always tell you if you’ve written code that will result in a
memory leak.
The CComPtr
, CComQIPtr
, and
CComGITPtr
smart pointer classes ease, but do not totally
alleviate, the resource management needed for interface pointers.
These classes have numerous useful methods that let you write more
application code and deal less with the low-level
resource-management details. You’ll find smart pointers most useful
when you’re using interface pointers with exception handling.
Finally, you can control memory management in
ATL with IAtlMemMgr
and five concrete implementations that
are supplied: CWin32Heap
, CComHeap
,
CCRTHeap
, CLocalHeap
, and CGlobalHeap
.
You can program ATL classes such as CString
to use memory
from these heaps or even from a custom implementation of
IAtlMemMgr
, to provide a high degree of control and
facilitate any number of performance optimizations.