This article takes a sideways glimpse at the auto_ptr class from the standard C++ library. It does this by considering the forces that shaped auto_ptr, and implements a class that seems to resolve those forces sensibly. However, the resulting class turns out to be very different to auto_ptr. Why is this? In all honesty I have to say that I feel it's because auto_ptr has lost its way and has turned into something of a dog's breakfast.
The basic thing to remember with auto_ptr is that it is supposed to improve the safety of handling of dynamically allocated objects within block structured code. What does that mean? Let's look at an example...
file_spy* smiley = new file_spy(top_secret);
smiley->espionage();
The problem with this code is that espionage() may throw an exception. If it does then the stack will unwind past smiley and we will have a memory leak. An answer to this problem is well known. You create a class whose destructor deletes the dynamically allocated object [1]. For example...
template<typename type>
class owner {
public:
owner( type* acquire )
: resource(acquire) {}
~owner() { delete resource; )
public: // query
type* get() const { return resource; }
type* operator->() const
{ return resource; }
type& operator*() const
{ return *resource; }
private: // state
type* resource;
};
Now the example can be re-written as...
owner<file_spy>
smiley( new file_spy(top_secret) );
...
smiley->espionage();
...and if espionage() throws an exception, the owner destructor will delete the dynamic file_spy, thus plugging the memory leak. The interesting part is when you try to flesh out owner, to make it "minimal but complete". The most obvious missing parts are the copy constructor and the assignment operator. The usual signatures for these are...
owner( const owner& rhs );
owner& operator=( const owner& rhs );
The problem with these two is the presence of the const in both signatures. Suppose you write the copy constructor like this...
owner( const owner& rhs )
: resource(rhs.resource) {)
You now have two pointers pointing at one dy namically allocated object. This is a sure fire way to create a dangling pointer. For example...
void dangle( owner<snafu> fubar )
{
...
}
owner<snafu> oops(new snafu(1,2));
dangle(oops);
Here dangle will copy construct the fubar parameter from oops, and when dangle returns it will call the destructor for the copy constructed fubar parameter, thus deleting the snafu pointed to by oops. When oops goes out of scope it too will have its destructor called and it too will try to delete the object already deleted by the dangle parameter. How can you solve this problem? One way is to keep a count of how many owners are pointing to the resource. This is reference counting. However, owner takes a different approach. Instead owner ensures that a dynamically allocated object is only ever pointed to by one owner. In other words if you write this...
snafu* p = new snafu(l,2);
owner<snafu> alpha(p); owner<snafu> beta(alpha);
Then owner has to ensure that either alpha or beta ends up owning p, but not both. Given this it might seem logical to implement the copy constructor like this...
owner( const owner& rhs )
{
type* ptr = rhs.resource;
rhs.resource = 0;
resource = ptr;
}
The idea here is that rhs passes the resource on to the newly constructed object. This won't work. The problem is that rhs is declared const, so the compiler will not allow the line...
rhs.resource = 0;
For this reason, suppose you give non-const references to the copy constructor and the as signment operator...
owner( owner& rhs)
{
type* ptr = rhs.resource;
rhs.resource =0;
resource = ptr;
}
owner& operator=(owner& rhs )
{
if (resource != 0)
delete resource;
type* ptr = rhs.resource;
rhs.resource = 0;
resource = ptr;
return *this;
}
This solves the problem, but up pops another (an ominous sign). The apparently legal code...
owner<snafu> f(); // function returning
// owner<snafu>
owner<snafu> delta( f() ); // copy
// constructor
...fails to compile. It fails because the copy constructor argument is an unnamed temporary object returned by f() and the C++ standard states that temporary objects cannot be bound to non-const reference parameters [2]. Okay I hear you say, that's easy to work round, make the parameter a named non-temporary object like this...
owner<snafu> tmp;
tmp = f();
owner<snafu> delta(tmp);
This won't work either. The reason is that the assignment is really just syntactic sugar for...
tmp.operator=( f() );
and, once again, the parameter is an unnamed temporary. So, giving a non-const reference to the copy constructor and assignment operator of owner has had a surprising result. It has made it impossible to return an owner object from a function by value. At this point I hope you feel owner is losing its way. It seems to be lurching from one problem to another, and the hack-work-rounds are simply making matters worse. Time to take a step back and start to think again. Remember that the original motivation was to improve the exception safety of dynamically allocated objects in block structured code. In this light, passing an owner object around as the return value from a function (or by value into a function) can be seen as an attempt to misuse owner. A reasonable approach is therefore to revoke the copy con structor and the assignment operator completely. One way to do this is to inherit the inability [3] to copy...
class no_copy
{
public:
no_copy() {}
private:
//NO definition for these
no_copy( const no_copy& );
no_copy& operator=( const no_copy& );
};
template<typename type>
class owner : private no_copy
{
...
};
What else needs doing to make owner more complete? Well, it seems unreasonable not to provide a default constructor. The easiest way to do this is to provide a default value to the constructor. It also seems reasonable to provide methods to explicitly control ownership.
template<typename type>
class owner : private no_copy
{
public: // construction/destruction
explicit owner( type* acquire=0 )
: resource(acquire) {}
~owner() ( delete resource; }
public: // query
type& operator*() const
{ return *resource; }
type* operator->() const
{ return resource; }
type* get() const { return resource; }
public: // services
void transfer( owner<type>& a )
{
reset(a.release());
}
void swap( owner<type>& a )
{
::swap(resource, a.resource);
}
void reset( type* ptr=0 )
{
delete resource;
resource = ptr;
}
type* release()
{
type* ptr = resource;
resource = 0;
return ptr;
}
private: // state
type* resource;
};
Finally let's see a small example of owner in use. Suppose we have a design where a publisher object is responsible for notifying all registered subscriber objects when a resource changes [4]. Further, suppose that the subscriber objects need only retain a single copy of the most up to date resource.
class subscriber
{
public:
//...
void notify( const resource& update )
{
// possible multi-thread lock here
latest.reset( new resource(update) );
}
//...
private:
owner<resource> latest;
};
[1] The C++ Language. 2nd edition, Bjarne Stroustrup, Addison Wesley, ISBN 0-201-53992-6, 9.4 Resource Acquisition p308
also
The Design and Evolution of C++, Bjarne Stroustrup, Addison Wesley, ISBN 0-201-54330-3, 16.5 Resource Management p388
[2] Many C++ compilers do not implement this yet.
[3] You can inherit inability as well as ability. For example you can inherit the inability to distinguish certain colours. It's called colour blindness.
[4] Design Patterns, Gamma et al, Addison Wesley, ISBN 9-201-63361-2, Observer p293
Overload Journal #17/18 - Jan 1997 + Programming Topics
Browse in : |
All
> Journals
> Overload
> 1718
(9)
All > Topics > Programming (877) Any of these categories - All of these categories |