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:
STDMETHODIMP CMyClass::SomeFunc() {
CFoo* pFoo = new Foo(); // instantiate C++ class
CAutoPtr<CFoo> spFoo(pFoo); // take ownership of pFoo
spFoo->DoSomeFoo();
// ... do other things with spFoo
} // CAutoPtr deletes pFoo instance
// 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.
CAutoPtr() : m_p( NULL ) { }
template< typename TSrc >
CAutoPtr( CAutoPtr< TSrc >& p ) {
m_p = p.Detach(); // Transfer ownership
}
CAutoPtr( CAutoPtr< T >& p ) {
m_p = p.Detach(); // Transfer ownership
}
explicit 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:
CAutoPtr<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:
class CAnimal { ... };
class CDog : public CAnimal { ... };
//
...
CDog* pDog = new CDog();
CAutoPtr<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.
~CAutoPtr() {
Free();
}
void Free() {
delete m_p;
m_p = NULL;
}
CAutoPtr
Operators
CAutoPtr defines two assignment
operators:
template< typename TSrc >
CAutoPtr< T >& operator=( CAutoPtr< TSrc >& p ) {
if(m_p==p.m_p) {
ATLASSERT(FALSE);
} else {
Free();
Attach( p.Detach() ); // Transfer ownership
}
return( *this );
}
CAutoPtr< T >& operator=( CAutoPtr< T >& p ) {
if(*this==p) {
if(this!=&p) {
ATLASSERT(FALSE);
p.Detach();
} else {
}
} else {
Free();
Attach( p.Detach() ); // Transfer ownership
}
return( *this );
}
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:
class CAnimal { ... };
class CDog : public CAnimal { ... };
// ...
// instantiate a CAnimal
CAutoPtr<CAnimal> spAnimal(new CAnimal());
// instantiate a CDog
CAutoPtr<CDog> spDog(new CDog());
// CAnimal instance freed here
spAnimal = spDog;
// ... CDog instance will be freed when spAnimal
// 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 (->):
operator T*() const {
return( m_p );
}
T* operator->() const {
ATLASSERT( m_p != NULL );
return( m_p );
}
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:
class CDog {
public:
void Bark() {}
int m_nAge;
};
CAutoPtr<CDog> spDog(new Dog);
spDog->Bark();
spDog->m_nAge += 5;
Finally, CAutoPtr defines operator==
and operator!= to do comparisons between two CAutoPtr
objects.
bool operator!=(CAutoPtr<T>& p) const { return !operator==(p); }
bool 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.
bool Allocate( size_t nElements ) {
ATLASSERT( m_p == NULL );
ATLTRY( m_p = new T[nElements] );
if( m_p == NULL ) {
return( false );
}
return( true );
}
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:
class CAnimal { public: void Growl() {} };
// each instance is of type CAnimal
CAutoVectorPtr<CAnimal> spZoo;
// allocate and initialize 100 CAnimal's
spZoo.Allocate(100);
Note that CAutoVectorPtr does not
overload the member access operator (->), as did
CAutoPtr. So, you cannot write code like this:
spZoo->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:
((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:
class CAnimal {
public:
CAnimal(int nAge) : m_nAge(nAge) {}
void Growl() {}
private:
int m_nAge;
}
CAutoVectorPtr<CAnimal> spZoo;
spZoo.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:
class CAnimal {};
// allocate only a single instance
CAnimal* pAnimal = new Animal;
CAutoVectorPtr<CAnimal> spZoo;
// wrong, wrong!!! pAnimal is not a collection
spZoo.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.
|