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
VARIANTmust be initialized before use by calling theVariantInitfunction on it. Alternatively, you can initialize the type and associated value field to a valid state (such as setting thevtfield toVT_EMPTY).A
VARIANTmust be copied by calling theVariantCopyfunction on it. This performs the proper shallow or deep copy, as appropriate for the data type stored in theVARIANT.A
VARIANTmust be destroyed by calling theVariantClearfunction on it. This performs the proper shallow or deep destroy, as appropriate for the data type stored in theVARIANT. For example, when you destroy aVARIANTcontaining aSAFEARRAYofBSTRs,VariantClearfrees eachBSTRelement in the array and then frees the array itself.A
VARIANTcan optionally represent, at most, one level of indirection, which is specified by adding theVT_BYREFbit setting to the type code. You can callVariantCopyIndto remove a single level of indirection from aVARIANT.You can attempt to change the data type of a
VARIANTby 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
vtmember of theVARIANTstructure 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
AddRefon 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:
Releasethe current interface pointer when it’s non-NULL.AddRefthe 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
DISPIDor name, passing zero parameters.Call a method by
DISPIDor name, passing one parameter.Call a method by
DISPIDor name, passing two parameters.Call a method by
DISPIDor 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.