|
|||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||
|
The auto_ptr Class Template
|
||||||||||||||||||
The auto_ptr Class Template
C++ Report, November/December 1998
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: *ip = 1; foo(ip); ... 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:
foo(ip); ... foo(ip); ... 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. auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T>& rhs) { if (this != &rhs) { delete pointer; pointer = rhs.pointer; rhs.pointer = 0; } return *this; } Summing it up again, strict ownership as used with the auto_ptr means that
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<int> api1(new int(1)); *aps1 = "world"; // line 1 if (aps1->size() < 16) // line 2 { auto_ptr<int> api2(api1); // line 3 ... } // line 4 ... False Use of auto_ptr s The following example shows two common newbie errors: int i = 2; auto_ptr<string> aps1(sp); auto_ptr<string> aps2(sp); // line 5 - ERROR !!! auto_ptr<int> api(&i); // line 6 - ERROR !!! 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: auto_ptr<string> aps(new string("hello world")); foo(aps); ... 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: { 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; ... { *sp = "my text"; } ... auto_ptr<string> aps(new string("hello world")); cout << "before foo: " << *aps; foo(aps); cout << "after foo: " << *aps; ... 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:
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. { auto_ptr<string> aps(new string("hello world")); g(aps); // might throw an exception } 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 Derived : public Base {}; void foo(auto_ptr<Base>); auto_ptr<Derived> apd(new Derived); ... foo(adp); 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: v[0] = static_cast<auto_ptr<int> > (new int(32)); ... v[0] = static_cast<auto_ptr<int> > (new int(64)); 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: auto_ptr<T>:: auto_ptr<T>(const auto_ptr<T>& rhs) // just a try !!! { pointer = rhs.pointer; rhs.pointer = 0; } void foo(const auto_ptr<T>& atp) { auto_ptr<T> newAtp(atp); } ... auto_ptr<int> api(new int(2)); foo(api); ... 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> api( f() ); // line 7 ... 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 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(); };
... auto_ptr<int> api( f() ); // line 7 ...
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/
/2/
/3/
|
|||||||||||||||||||
© 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 |