previous page
next page

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.


previous page
next page
Converted from CHM to HTML with chm2web Pro 2.75 (unicode)