Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
CONTACT 
The auto_ptr Class Template

The auto_ptr Class Template
The auto_ptr Class Template

C++ Report, November/December 1998
Klaus Kreft & Angelika Langer


 

A small and innocently looking library component, namely the auto_ptr class template, kindled an amazing number of passionate discussions on the standards committee. It was designed, and redesigned, at one point almost dropped from the standard library, and until virtually the last minute of the standardization process it was subject to changes and refinements. Much ado about almost nothing? In this column let us see what the auto_ptr is.

The auto_ptr class template is an abstraction that eases the use of pointers referring to objects on the heap. Long before the auto_ptr was introduced into the standard, the C++ community had already used an idiomatic abstraction that supports the usage of pointers referring to heap objects: the smart pointer . Our experience from teaching standard library is that people who are not yet familiar with the auto_ptr class template conjecture that the auto_ptr is some kind of standard smart pointer implementation. That's not what it is; it has completely different semantics. To address this delusion, we do not only explain the auto_ptr in this article, but also discuss the differences and similarities of smart pointers and the auto_ptr class template. Hence, let us start with a recap of smart pointers.

Smart pointers

In the old days of C programming, passing a pointer to heap memory as an argument to a function often introduced some uncertainties. Consider the following example:

int *ip = malloc (sizeof(int));
*ip = 1;
foo(ip);
...
Without comprehensive documentation of foo() you are neither sure whether you can still access *ip after invocation of foo() nor do you know whether it is your responsibility to free the memory afterwards or whether it has already been freed by foo(). Passing such pointers to heap memory through several layers of functions makes things even more complicated. Also, when new features require changes of the old memory management policy the entire issue often turns into a maintenance nightmare.

In the early days of C++, when it was a new language used predominantly by former C programmers, traditional C-style implementation techniques were still popular. Naturally, these programmers faced the same problems as before with C. After some time the C++ community matured. Programmers began to understand the power and versatility of user-defined types and developed C++ idioms that helped to overcome some of the problems that stemmed from the C heritage. One of these idioms is the smart pointer . It eliminates memory management problems like the one described above. The principle of a smart pointer implementation is as follows:

  • A smart pointer has a built-in pointer as a data member. This pointer refers to the heap object. The built-in pointer is the smart pointer’s constructor argument. Typically the smart pointer is a class template with the base type of the built-in pointer as template parameter, e.g. if the built-in pointer is of type X* , then X is the template argument.
  • The dereferencing operators operator->() and operator*() are defined and implemented by forwarding their operations to the respective operators -> and * of the built-in pointer data member. This design allows the same access syntax for smart pointers as for built-in pointers.
  • The smart pointer counts the number of references to the heap object by a smart implementation of the copy constructor, assignment operator, and destructor. If, and only if, the number of references drops to zero, the referenced heap object is freed. In a more abstract sense it means that the object on the heap can be shared by different parts of the program. The smart pointer keeps track of these "client" parts, so that it deletes the heap object when none of them is referring the heap object anymore. This aspect of the smart pointers semantics is essential, especially in comparison to the semantics of auto_ptr .
Detailed discussions of the design and implementation of smart pointers can be found in / 1 / and / 2 /. By now, the smart pointer idiom has become a well known and often used idiom in the C++ community. It turned out to be also valuable in the presence of exceptions because it incorporates the ‘resource acquisition is initialization’-idiom (see / 3 /). Lets have a look at an example: int *ip = new int(1);
foo(ip);
...
When foo() throws an exception, we have to free the memory referenced by ip to prevent a memory leak. One way to do this is to wrap the call to foo() into a try -block and the deallocation of the memory into the corresponding catch -block. While this is a correct solution it is not overly elegant and can get complicated when the code gets more complex. Let’s see what happens when we use a smart pointer instead of a built-in pointer: smart_ptr<int *> sip = new int(1);
foo(ip);
...
Now, we do not have to use a try -block at all. The destructor of the smart pointer is called during stack unwinding, which is triggered by the exception, and the reference counter is decremented. As the heap object is referenced only once, the reference counter drops to zero and the memory is freed.

The auto_ptr Class Template

While the smart pointer significantly eases the use of objects allocated on the heap, it does not come for free: It introduces a performance penalty compared to a built-in pointer. Take for example the assignment. For a built-in pointer it is a simple assembler operations that results in a binary copy of the pointer value into some other memory location. The assignment operator for a smart pointer, in constrast, is a user-defined member function, which has to maintain the reference counter of the right hand and left hand argument, including the check if the left hand counter has dropped to zero and the referenced object has to be deleted.

Efficiency is one of the main design goals of the standard library. When the C++ committee looked for a helpful pointer abstraction that could be included in the standard , people were very much aware of the performance penalty introduced by a smart pointer. While they wanted to save some of the smart pointer’s advantages, in particular the ability to handle the memory management in case of exceptions, they wanted to minimize the performance penalty. The pointer abstraction that made it into the standard is the class template auto_ptr. As a smart pointer, it helps to ease the use of objects on the heap. To minimize the performance penalty, its design does not use reference counting. Instead, the auto_ptr is based on the idea of strict ownership . This leads to completely different semantics for the auto_ptr compared to a smart pointer.

Strict Ownership

The principle of strict ownership can be explained best by taking a detailed look at the design of the auto_ptr class template. Like the smart pointer, the auto_ptr has the original built-in pointer as data member and the auto_ptr , too, is a class template whose template parameter is the base type of the contained built-in pointer. The constructor receives this pointer referring to an object on the heap, and obtains ownership of the referred object. Figure 2 below shows a possible implementation of the auto_ptr ’s assignment operator.

tempate<classT>
auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T>& rhs)
{
  if (this != &rhs)
  {
    delete pointer;
    pointer = rhs.pointer;
    rhs.pointer = 0;
  }
  return *this;
}
The assignment operator deletes the left hand side’s pointer data member, passes the right hand side’s pointer data member to the left hand side, and sets the right hand side’s pointer data member to 0 . Thus the left hand side can now access the referenced object on the heap, while the right hand side cannot access it any longer: the ownership of the referred object has passed from the right hand side to the left hand side. The copy construction works in a similar way: the ownership is passed from the already existing object, which is passed in as the constructor argument, to the newly constructed object. The destructor simply delete s the pointer data member, i.e. when the owning object is destroyed also the owned object on the heap is destroyed and its memory is released.

Summing it up again, strict ownership as used with the auto_ptr means that

  • One and only one auto_ptr object owns the referred object on the heap.
  • Copy construction and assignment pass the ownership from one auto_ptr object to the other.
  • When an auto_ptr is destroyed it does not only give up the ownership, but also destroys the owned object.
Correct Use of auto_ptr s

Let’s discuss the usage of the auto_ptr by means of some examples. This also reveals the subtleties of the strict ownership principle.

auto_ptr<string> aps1(new string("hello"));
auto_ptr<int> api1(new int(1));
*aps1 = "world"; // line 1
if (aps1->size() < 16) // line 2
{
  auto_ptr<int> api2(api1); // line 3
  ...
} // line 4
...
Line 1 and 2 show that the operator*() and operator->() can be applied to the auto_ptr in the same way and with the same semantics as they can be applied to a built-in pointer. The auto_ptr defines operator*() and operator-> () by forwarding them to the respective operations for built-in pointers. In line 3 the ownership of the integer on the heap swaps from api1 to api2 . This means that, below line 3, api1 cannot be used to access the integer on the heap any longer. The transfer of the ownership from api1 to api2 also effects the lifetime of the integer on the heap: it is destroyed at the end of the block that follows the if -statement, i.e. in line 4.

False Use of auto_ptr s

The following example shows two common newbie errors:

string *sp = new string("hello world");
int i = 2;
auto_ptr<string> aps1(sp);
auto_ptr<string> aps2(sp); // line 5 - ERROR !!!
auto_ptr<int> api(&i); // line 6 - ERROR !!!
As already mentioned, strict ownership means that exactly one auto_ptr is referring to the object on the heap. This implies that a programmer is allowed to initialize only one auto_ptr object with the same built-in pointer referring a certain heap object. Not obeying this rule (as demonstrated in line 5) can lead to severe errors at runtime. The reason is that the behavior of the auto_ptr ’s destructor: It unconditionally deletes the object on the heap. Having multiple auto_ptr s referring to the same heap object results in premature or multiple deletions of the heap object, each deletion occurring when the respective auto_ptr goes out of scope.

Another programming error, that might lead to disastrous runtime behavior, is the construction of an auto_ptr with a pointer that is not referring to heap memory, as shown in line 6. Again, the reason is the auto_ptr destructor’s behavior: in this case it attempts to delete an object on the stack when api goes out of scope.

auto_ptr s as Input Function Arguments

In our next examples we examine situations where we want to pass an auto_ptr as a function argument. Say, we have the following function:

void foo(auto_ptr<string> sp) { cout << *sp; } What will happen if we call foo() from another function: ...
auto_ptr<string> aps(new string("hello world"));
foo(aps);
...
Because the auto_ptr<string> is passed by value, its copy constructor is invoked, which passes the ownership of the string object on the heap to the local auto_ptr<string> object sp , that is passed to foo() as its argument. One of the consequences is that the calling function cannot access the string on the heap anymore after the the invocation of foo(). The other consequence is that the string on the heap is destroyed when foo() returns to the calling function. There are situations where this is indeed the intended behavior: whenever the object on the heap is used solely as an input parameter to the called function. In other situations the effect of passing the ownership to a temporary function argument might not be desired, though.

auto_ptr s as Output Function Arguments

To boot, pointers are used in other situations, too. One typical way is to use them is as an output parameter: The function receives a pointer to an object and changes the object via the pointer. After the control flow returns to the calling function, the modifications are visible to the caller and the changed object continues to be used. If we can pass in built-in pointers as output arguments of functions, what would be more intuitive than passing in an auto_ptr in order to achieve the same "output argument" effect?

As we explained before, an auto_ptr passed by value cannot be used as an alternative to a built-in pointer in such a situation, because the heap object would be deleted on return from the invoked function. The equivalent to a built-in pointer is an auto_ptr object passed by reference in that case. Depending on the intended use even an auto_ptr passed by const reference might make sense. Let’s see an example for each case. This is the first one:

void foo(auto_ptr<string>& sp)
{ sp = static_cast<auto_ptr<string> > (new string("my text")); }
...
auto_ptr<string> aps(new string("hello world"));
cout << "before foo: " << *aps;
foo(aps);
cout << "after foo: " << *aps;
...
And here is the second one: void foo(const auto_ptr<string>& sp)
{ *sp = "my text"; }
...
auto_ptr<string> aps(new string("hello world"));
cout << "before foo: " << *aps;
foo(aps);
cout << "after foo: " << *aps;
...
The text that is written to cout is the same in both cases: first " hello world" and then "my text" . The difference lies in the way foo() binds the new text "my text" to the output parameter sp . In the first case, foo() creates a new string on the heap. By assigning it to sp the heap object formerly referred to by sp is deleted. That’s why we pass a non- const reference to an auto_ptr object to the function foo(): we want to allow that it is altered. In the second case, where we pass a const reference to foo() , the function does not change the auto_ptr object, but the string on the heap that is referred to by the auto_ptr . The standard committee used a clever design to make the const reference to an auto_ptr work in the way described above. We explain the trick later in this article. For now, we continue with the explanation of the auto_ptr ’s usage and interface.

Explicitly Changing Ownership

So far, the examples might have given you a feeling for the rigid pattern that governs the usage of the auto_ptr : Construct an auto_ptr object with a pointer to a heap object to establish the ownership; optionally pass the ownership of the heap object to another auto_ptr , and eventually delete the heap object, which happens implicitly when the owning auto_ptr goes out of scope. There are three member functions that can be used to explicitly break this pattern:

  • get() , returns the built-in pointer that is contained in the auto_ptr ,
  • release() , returns the built-in that is contained in the auto_ptr and sets this data member to 0 ,
  • reset() , receives a pointer referring to a heap object as parameter and assigns this pointer to the data member after deletion of the previously contained built-in pointer.
While these member functions make the auto_ptr more flexible, they also make its usage more complicated. Our advice is: do not use them if you can live without them.

auto_ptr s and Exceptions

We already mentioned briefly that one design goal of the pointer abstraction for the standard library is to aid the deallocation of heap memory in case of exceptions. The auto_ptr meets this requirement thanks to the strict ownership concept: when an auto_ptr ’s destructor is called during stack unwinding, it deletes the heap object it owns. Looking at the example below, we see that we neither need a try -block that surrounds the invocation of g() nor a catch catch -block that frees the allocated string. Instead, when an exception is thrown by g() the destructor of aps will free the string.

void f()
{
  auto_ptr<string> aps(new string("hello world"));
  g(aps); // might throw an exception
}
More examples of sensible uses of the auto_ptr class template in conjunction with exception handling can be found in / 4 /.

auto_ptr Conversions

auto_ptr s pointing to different types of heap objects can be converted into each other as long as the underlying pointers types are convertible. For example, we can do the following:

class Base {};
class Derived : public Base {};
void foo(auto_ptr<Base>);
auto_ptr<Derived> apd(new Derived);
...
foo(adp);
The function foo() requires an argument of type auto_ptr<Base> ; the argument provided is of type auto_ptr<Derived> . Normal derived class pointers can be converted to base class pointers. Similarly, an auto_ptr to a derived class object can be converted to an auto_ptr to a base class object. Such conversions are defined by a constructor template and a cast operator template.

auto_ptr s and STL Containers

Did you notice that in our code examples all instances of auto_ptr are automatic variables? They are never used as static variables, or as data members of classes, or created on the heap. This begs the question whether or not it makes sense to use auto_ptr s for anything else but local automatic variables on the stack of some function? There is no straight answer to this question. The auto_ptr is attractive in some situations due to its ability to aid proper memory management, especially in presence of exceptions. What makes the auto_ptr unattractive in other situations is the ownership transfer of the pointed to heap object during copy construction and assignment. Lets see an example that demonstrates both aspects. Say, we want to use a vector of int pointers, i.e. vector<int*>. As we aim to introduce a reliable policy for the memory management of the pointed to heap objects, we assume that it is a good idea to use a vector of auto_ptr<int> , i.e. vector<auto_ptr<int> > , instead. Now we can do the following:

vector<auto_ptr<int> > v;
v[0] = static_cast<auto_ptr<int> > (new int(32));
...
v[0] = static_cast<auto_ptr<int> > (new int(64));
We do not have to worry about the deletion of the int on the heap referred to by v[0] when we assign a new int to v[0] . This example does not only look good, it also compiles - but it is incorrect! In fact, the behavior of the code is undefined. The reason is simple: The standard library’s containers require that their element types must be copy constructible. Copy constructible basically means that an object and a copy of that object are equivalent. This is not true for auto_ptr objects because of the strict ownership principle. An auto_ptr and its copy differ in that one owns the heap object and the other does not. What would be needed in the example above were a smart pointer instead of an auto_ptr . A smart pointer is copy constructible: A smart pointer and its copy are equivalent because they share ownership of the same heap object.

The conclusion is that although the auto_ptr is the pointer abstraction in the standard library that aids the memory management of heap objects, there are situations where the auto_ptr is not the appropriate abstraction to use. When the auto_ptr is considered not suitable then it is usually because of the semantics of its copy construction or assignment. Whenever you are about to built a solution using the auto_ptr , keep the strict ownership principle in mind and check whether the auto_ptr 's copy and assignment semantics are what you need.

Details of the auto_ptr Implementation

It took the standard committee some last minute refinements to make the auto_ptr work correctly under all conditions. We mentioned earlier that correct copy construction of const and non-const auto_ptr objects is not trivial and involves some tricky design issues. So far, we have spared you the details. Let us now take a look at the implementation of the autoptr 's copy semantics.

We'll start our discussion with a canonically implemented copy constructor:

tempate<classT>
auto_ptr<T>:: auto_ptr<T>(const auto_ptr<T>& rhs) // just a try !!!
{
  pointer = rhs.pointer;
  rhs.pointer = 0;
}
As expected, this implementation passes the ownership of the heap object to the newly constructed auto_ptr object. Surprisingly, this copy constructor has a subtle problem with const -correctness, which we can see by considering the following function template: tempate<classT>
void foo(const auto_ptr<T>& atp)
{
  auto_ptr<T> newAtp(atp);
}
...
auto_ptr<int> api(new int(2));
foo(api);
...
The function foo() receives a const reference to an auto_ptr as parameter and does nothing except constructing a new auto_ptr as a copy of the received function argument. As the copy constructor passes the ownership of the heap object to the newly constructed auto_ptr , the original auto_ptr which foo() received as a const reference, loses the ownership. This means that after invocation of foo() , we cannot access the heap object any longer because the auto_ptr we provided to foo() does not own it any more. In other words, the auto_ptr object, that we passed to the function foo() via a const reference, is modified, and this is not exactly what we expect when we pass a const reference as a parameter to a function.

Obviously, this implementation of the copy constructor is incorrect. How can we fix it? Let us make the copy constructor’s parameter a non-const reference. As a consequence, foo() also has to make its parameter a non-const reference when it wants to invoke the copy constructor. The signature of the function foo() would then clearly express that the function argument will be modified by the function and no silent changes of constant auto_ptr objects can happen anymore.

What are the consequences of providing a copy constructor that takes a non- const reference? The problem is that such a copy constructor is not sufficient for the implementation of the auto_ptr ’s copy semantics, in particular is does not allow copies of rvalues. In order to copy an rvalue a copy constructor, that takes a const reference, is required. Consider the following situation:

auto_ptr<int> f();
...
auto_ptr<int> api( f() ); // line 7
...
Line 7 does not compile because the return code of the function f() is a rvalue ,and rvalues cannot be bound to non-const references, but our copy constructor requires a non-const reference as an argument. It is certainly not trivial to figure out a solution for this problem. On the one hand, we try to avoid a copy constructor that takes a const reference, while on the other hand such a copy constructor is needed to copy-construct rvalues.

The solution, that works for the auto_ptr , is to allow the 'copy construction from rvalues' by means other than a copy constructor. Instead of providing a copy constructor that takes a const reference, a helper class is introduced along with conversions to and from this new type. The auxiliary class is a nested class called auto_ptr_ref, which holds a reference to an auto_ptr as data member. The conversion from an auto_ptr to an auto_ptr_ref is defined by means of a cast operator auto_ptr::operator auto_ptr_ref() and back from an auto_ptr_ref to an auto_ptr by means of a converting constructor auto_ptr::auto_ptr(auto_ptr_ref) .

The code sample below shows the declaration of the auto_ptr class template as specified in the standard; the portions that have to do with the helper class are high-lighted.

template<class X> class auto_ptr {
  template <class Y> struct auto_ptr_ref {};
public:
  typedef X element_type;
  // construct/copy/destroy:
  explicit auto_ptr(X* p =0) throw();
  auto_ptr(auto_ptr&) throw();
  template<class Y> auto_ptr(auto_ptr<Y>&) throw();
  auto_ptr& operator=(auto_ptr&) throw();
  template<class Y> auto_ptr& operator=(auto_ptr<Y>&) throw();
  ~auto_ptr() throw();
  // members:
  X& operator*() const throw();
  X* operator­>() const throw();
  X* get() const throw();
  X* release() throw();
  void reset(X* p =0) throw();
  // conversions:
  auto_ptr(auto_ptr_ref<X>) throw();
  template<class Y> operator auto_ptr_ref<Y>() throw();
  template<class Y> operator auto_ptr<Y>() throw();
};


Now, when an auto_ptr object returned from a function shall be used to copy-construct another auto_ptr object, as in our example above, a conversions takes place instead of a copy construction, because the normal copy constructor is not available. Let's reconsider the example:

auto_ptr<int> f();
...
auto_ptr<int> api( f() ); // line 7
...
In line 7 the compiler is asked to construct an auto_ptr object from an auto_ptr rvalue, namely the object returned from f() . The only viable constructor is auto_ptr::auto_ptr(auto_ptr_ref) . Hence, the compiler looks for a conversion sequence that turns the auto_ptr rvalue into an auto_ptr_ref object. This conversion is achieved via the cast operator, which creates an auto_ptr_ref that holds a reference to the auto_ptr rvalue. This temporary auto_ptr_ref object is eventually used to construct the auto_ptr object api . (Note, that the latter construction, as usual for auto_ptr s, transfers the ownership of the heap object from the auto_ptr reference contained in auto_ptr_ref to the newly constructed auto_ptr object.)

To sum it up: Different from normal classes, the auto_ptr class template has a copy constructor that takes a non- const reference and does not have a "normal" copy constructor that takes a const reference. The normal copy constructor was eliminated to solve the const -correctness problem: Without the normal copy constructor, constant auto_ptr objects cannot be copied, which is desirable because the copy construction would modify the constant auto_ptr , namely take away its ownership property. The compiler, however, needs a way to create copies of auto_ptr rvalues and this copy construction of rvalues is usually performed by means of a "normal" copy constructor. As there is not normal copy constructor, the compiler performs conversions to and from the helper class auto_ptr_ref , as described above.
 
 

What do we learn from this complex implementation of an almost trivial class? Well, the crux with auto_ptr is that it does not have normal copy semantics due to the strict ownership concept. This anomaly becomes apparent when you think of the fact that the auto_ptr does not meet the copy constructible requirements for element types of containers in the standard library. The special copy semantics are also visible in the signature of the auto_ptr 's copy constructor.

Summary

The auto_ptr is the standard library’s pointer abstraction that aids memory management of heap objects. While it is highly efficient, it has a special behavior based on strict ownership. This behavior makes it useful for automatic variables, function parameters, and return codes, that are pointers to heap objects. Using references and const references to an auto_ptr extends the auto_ptr ’s usability.

Compared to a smart pointer the auto_ptr is more efficient. Its special behavior, however, limits its applicability somewhat in comparison to a smart pointer.

References

/1/ 
Scott Meyers
More Effective C++
Addison-Wesley, 1996

/2/ 
John J. Barton and Lee R. Nackman
Scientific and Engineering C++
Addison-Wesley, 1994

/3/ 
Bjarne Stroustrup
The C++ Programming Language
Addison-Wesley, 1997

/4/ 
Jack W. Reeves

  • Coping with Exceptions, C++ Report, March 1996
  • Exceptions and Standards, C++ Report, Mai 1996
  • Ten Guidelines for Exception Specification, C++ Report, July 1996
  • Exceptions and Debugging, C++ Report, November/December 1996

  •  

     
     
     
     
     
     
     

    If you are interested to hear more about this and related topics you might want to check out the following seminar:
    Seminar
     
    Effective STL Programming - The Standard Template Library in Depth
    4-day seminar (open enrollment and on-site)
    IOStreams and Locales - Standard C++ IOStreams and Locales in Depth
    5-day seminar (open enrollment and on-site)
     

     

      © Copyright 1995-2003 by Angelika Langer.  All Rights Reserved.    URL: < http://www.AngelikaLanger.com/Articles/C++Report/AutoPointer/AutoPointer.html  last update: 22 Nov 2003