Smart types


Smart types are objects that behave as types. They are C++ class implementations that encapsulate a data type, wrapping it with operators and functions that make working with the underlying type easier and less error prone. When these smart types encapsulate an interface pointer, they are referred to as smart pointers. Smart pointers work with the IUnknown interface to ensure that resource allocation and deallocation are correctly managed. They accomplish this by various functions, construct and destruct methods, and overloaded operators. There are numerous smart types available to the C++ programmer. The two main smart types covered here are Direct-To-COM and the Active Template Library.
DTC was an initiative from Microsoft to make COM C++ programming more like Visual Basic. To achieve this DTC provides a set of classes and compiler extensions that shipped initially with Visual Studio 5.
Smart types can make the task of working with COM interfaces and data types easier, since many of the API calls are moved into a class implementation; however, they must be used with caution and never without a clear understanding of how they are interacting with the encapsulated data type.
In this topic:

Direct-To-COM Smart Types

The smart type classes supplied with DTC are known as the Compiler COM Support Classes and consist of:
  • _com_error—This class represents an exception condition in one of the COM support classes. This object encapsulates the HRESULT and the IErrorInfo COM exception objects.
  • _com_ptr_t—This class encapsulates a COM interface pointer. See below for common uses.
  • _bstr_t—This class encapsulates the BSTR data type. The functions and operators on this class are not as rich as the ATL CComBSTR smart type; hence, this is not normally used.
  • _variant_t—This class encapsulates the VARIANT data type. The functions and operators on this class are not as rich as the ATL CComVariant smart type; hence, this is not normally used.
To define a smart pointer for an interface, you can use the macro _COM_SMARTPTR_TYPEDEF like this:
[VCPP]
_COM_SMARTPTR_TYPEDEF(IFoo, __uuidof(IFoo));
The compiler expands this as follows:
[VCPP]
typedef _com_ptr_t < _com_IIID < IFoo, __uuidof(IFoo) >  > IFooPtr;
Once declared, it is simply a matter of declaring a variable as the type of the interface and appending Ptr to the end of the interface. Below are some common uses of this smart pointer that you will see in the numerous C++ samples.
[VCPP]
// Get a CLSID GUID constant
extern "C" const GUID __declspec(selectany)CLSID_Foo =  \
  {
  0x2f3b470c, 0xb01f, 0x11d3, 
  {
    0x83, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
  }
};

// Declare Smart Pointers for IFoo, IBar, and IGak interfaces
_COM_SMARTPTR_TYPEDEF(IFoo, __uuidof(IFoo));
_COM_SMARTPTR_TYPEDEF(IBar, __uuidof(IBar));
_COM_SMARTPTR_TYPEDEF(IGak, __uuidof(IGak));

STDMETHODIMP SomeClass::Do()
{
  // Create Instance of Foo class and QueryInterface (QI) for IFoo interface
  IFooPtr ipFoo;
  HRESULT hr = ipFoo.CreateInstance(CLSID_Foo);
  if (FAILED(hr))
    return hr;

  // Call method on IFoo to get IBar
  IBarPtr ipBar;
  hr = ipFoo->get_Bar(&ipBar);
  if (FAILED(hr))
    return hr;

  // QI IBar interface for IGak interface
  IGakPtr ipGak(ipBar);
  // Call method on IGak
  hr = ipGak->DoSomething();
  if (FAILED(hr))
    return hr;

  // Explicitly call Release()
  ipGak = 0;
  ipBar = 0;
  // Let destructor call IFoo's Release
  return S_OK;
}
One of the main advantages of using the DTC smart pointers is that they are automatically generated from the #import compiler statement for all interface and coclass definitions in a type library. See section Importing ArcGIS Type Libraries for more details on this.
Beware! It is possible to create an object implicitly in a DTC smart pointer's constructor, for example:

IFooPtr ipFoo(CLSID_Foo)

However, this will raise a C++ exception if there is an error during object creation (for example, if the DLL containing the object implementation was accidentally deleted). This exception will typically be unhandled and cause a crash. A more robust approach is to avoid exceptions in COM, call CreateInstance explicitly, and handle the failure code, for example:

IFooPtr ipFoo;
HRESULT hr = ipFoo.CreateInstance(CLSID_Foo);
if (FAILED(hr))
  return hr; // return object creation failure code to caller

Active Template Library Smart Types

ATL defines various smart types, as seen in the list below. You are free to combine both the ATL and DTC smart types in your code. However, it is typical to use the DTC for smart pointers, as they are easily generated by importing type libraries. For BSTR and VARIANT types , the ATL versions for CComBSTR and CComVariant are typically used.
ATL smart types include:
  • CComPtr—encapsulates a COM interface pointer by wrapping the AddRef and Release methods of the IUnknown interface.
       
  • CComQIPtr—encapsulates a COM interface and supports all three methods of the IUnknown interface: QueryInterface, AddRef, and Release.
  • CComBSTR—encapsulates the BSTR data type.
  • CComVariant—encapsulates the VARIANT data type.
  • CRegKey—provides methods for manipulating Windows registry entries.
  • CComDispatchDriver—provides methods for getting and setting properties and calling methods through an object's IDispatch interface.
  • CSecurityDescriptor—provides methods for setting up and working with the Discretionary Access Control List (DACL).
This section examines the first four smart types and their uses. The example code below, written with ATL smart pointers, looks like the following:
[VCPP]
// Get a CLSID GUID constant
extern "C" const GUID __declspec(selectany)CLSID_Foo =  \
  {
  0x2f3b470c, 0xb01f, 0x11d3, 
  {
    0x83, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
  }
};

STDMETHODIMP SomeClass::Do()
{
  // Create Instance of Foo class and QI for IFoo interface
  CComPtr < IFoo > ipFoo;
  HRESULT hr = CoCreateInstance(CLSID_Foo, NULL, CLSCTX_INPROC_SERVER, IID_IFoo,
    (void **) &ipFoo);
  if (FAILED(hr))
    return hr;

  // Call method on IFoo to get IBar
  CComPtr < IBar > ipBar;
  HRESULT hr = ipFoo->get_Bar(&ipBar);
  if (FAILED(hr))
    return hr;

  // IBar interface for IGak interface
  CComQIPtr < IGak > ipGak(ipBar);

  // Call method on IGak
  hr = ipGak->DoSomething();
  if (FAILED(hr))
    return hr;

  // Explicitly call Release()
  ipGak = 0;
  ipBar = 0;

  // Let destructor call Foo's Release
  return S_OK;
}
Beware! The equality ("== ") operator may have different implementations when used during smart pointer comparisons. The COM specification states object identification is performed by comparing the pointer values of IUnknown. The DTC smart pointers will perform necessary QI and comparison when using the "==" operator. However, the ATL smart pointers will not do this, and you must use the ATL IsEqualObject() method.
The most common smart pointer seen in the Visual C++ samples is the DTC type. In the examples below, which illustrate the BSTR and VARIANT data types, the DTC pointers are used. When working with CComBSTR, use the text mapping L"" to declare constant OLECHAR strings. For more information on string data types see the section ATL String Types. CComVariant derives directly from the VARIANT data type, meaning that there is no overloading with its implementation, which in turn simplifies its use. It has a rich set of constructors and functions that make working with VARIANTs straightforward; there are even methods for reading and writing from streams. Be sure to call the Clear method before reusing the variable.
[VCPP]
ipFoo->put_Name(CComBSTR(L "NewName"));
if FAILED(hr)
)return hr; 

// Create a VT_I4 variant (signed long)
CComVariant vValue(12); 

// Change its data type to a string
  hr = vValue.ChangeType(VT_BSTR); if (FAILED(hr)
)return hr;
Some method calls in IDL are marked as being optional and take a variant parameter. However, in Visual C++, these parameters still have to be supplied. To signify that a parameter value is not supplied, a variant is passed specifying an error code or type DISP_E_PARAMNOTFOUND:
[VCPP]
CComBSTR documentFilename(L "World.mxd");

CComVariant noPassword;
noPassword.vt = VT_ERROR;
noPassword.scode = DISP_E_PARAMNOTFOUND;
HRESULT hr = ipMapControl->LoadMxFile(documentFilename, noPassword);
When working with CComBSTR and CComVariant, the Detach function releases the underlying data type from the smart type and can be used when passing a result as an [out] parameter of a method. The use of the Detach method with CComBSTR is shown below:
[VCPP]
STDMETHODIMP CFoo::get_Name(BSTR *name)
{
  if (name == 0)
    return E_POINTER;
  CComBSTR bsName(L "FooBar");
  *name = bsName.Detach();
}
Beware! CComVariant(VARIANT_TRUE) will create a short integer variant (type VT_I2) and not a Boolean variant (type VT_BOOL) as expected. You can use CComVariant(true) to create a Boolean variant.
Beware! CComVariant myVar(ipSmartPointer) will result in a variant type of Boolean (VT_BOOL) and not a variant with an object reference (VT_UNKNOWN) as expected. It is better to pass unambiguous types to constructors, that is, types that are not themselves smart types with overloaded cast operators.

// Perform QI if IUnknown
IUnknownPtr ipUnk = ipSmartPointer;
// Ensure IUnknown* constructor of CComVariantis used.
CComVariant myVar2(ipUnk.GetInterfacePtr());
A common practice with smart pointers is to use Detach to return an object from a method call. When returning an interface pointer, the COM standard is to increment reference count of the [out] parameter inside the method implementation. It is the caller's responsibility to call Release when the pointer is no longer required. Consequently, care must be taken to avoid calling Detach directly on a member variable. A typical pattern is shown below:

[VCPP]
STDMETHODIMP CFoo::get_Bar(IBar **pVal)
{
  if (pVal == 0)
    return E_POINTER;

  // Constructing a local smart pointer using another smart pointer
  // results in an AddRef (if pointer is not 0).
  IBarPtr ipBar(m_ipBar);

  // Detach will clear the local smart pointer and the 
  // interface is written into the output parameter.
  *pVal = ipBar.Detach();

  // This can be combined into one line 
  // *pVal = IBarPtr(m_ipBar).Detach(); 
  return S_OK;
}
The above pattern has the same result as the following code; note that a conditional test for a zero pointer is required before AddRef can be called.  Calling AddRef (or any method) on a zero pointer will result in an access violation exception and typically crash the application:
[VCPP]
STDMETHODIMP CFoo::get_Bar(IBar **pVal)
{
  if (pVal == 0)
    return E_POINTER;

  // copy the interface pointer (no AddRef) into the output parameter
  *pVal = m_ipBar;

  // Make sure interface pointer is nonzero before calling AddRef
  if (*pVal)
    *pVal->AddRef();

  return S_OK;
}
When using a smart pointer to receive an object from an [out] parameter on a method, use the smart pointer "&" dereference operator. This will cause the previous interface pointer in the smart pointer to be released. The smart pointer is then populated with the new [out] value. The implementation of the method will have already incremented the object reference count. This will be released when the smart pointer goes out of scope:
[VCPP]
{
  IFooPtr ipFoo1, ipFoo2;
  ipFoo1.CreateInstance(CLSID_Foo);
  ipFoo2.CreateInstance(CLSID_Foo);

  // Initialize ipBar Smart pointer from Foo1
  IBarPtr ipBar;
  ipFoo1->get_Bar(&ipBar);

  // The "&" dereference will call Release on ipBar
  // ipBar is then repopulated with a new instance of IBar
  ipFoo2->get_Bar(&ipBar);
}

// ipBar goes out of scope, and the smart pointer destructor calls Release