devxlogo

Automate Resource Management with shared_ptr

Automate Resource Management with shared_ptr

td::auto_ptr is the only smart pointer class available in C++98. Alas, since this class is neither assignable nor copy-constructible, creating containers of auto_ptr objects is illegal. This limitation has been a source of frustration and confusion for years. At last, the Library Extensions Technical Report 1 (TR1 for short) rectifies this embarrassment, adding a new smart pointer class called shared_ptr to the standard header . The following sections explain how to use shared_ptr to automate resource management. You will also learn how shared_ptr can simplify programming tasks that I’ve shown here before?such as simulating heterogeneous containers and implementing the Pimpl idiom.


How to combine smart pointers with STL containers and algorithms without risking undefined behavior?


Use the std::tr1::shared_ptr class.

Presenting the Problem
A typical smart pointer wraps a raw pointer to a dynamically allocated object and overloads the operators -> and *. The responsibility for deleting the pointer is delegated to the smart pointer’s destructor, thus freeing you from manual?and bug-prone?memory management. The problem, however, is that auto_ptr, the only smart pointer class available in C++98, has peculiar copy and assignment semantics. When you assign an auto_ptr x to another auto_ptr y, not only is the target object modified (as expected) but so is the source object, which becomes null. This behavior makes auto_ptr incompatible with STL containers and algorithms. Let’s see how shared_ptr solves this problem.

Author’s Note: To use shared_ptr you need to download its sources from Boost. Notice that this Solution adheres to the official TR1 Draft Proposal, which differs slightly from boost::shared_ptr.

Construction and Initialization
shared_ptr‘s default constructor creates what is known as an empty smart pointer, i.e., an object whose pointer is null. You can assign a valid pointer to it later:

class Foo{ int x;public: explicit Foo(int val=0) :x(val) {} void do_something() {/*..*/} int show_x() const {std::cout //for shared_ptrusing std::tr1::shared_ptr;int main(){ shared_ptr pf; //pf is an empty shared_ptr //bind a "live" pointer to pf pf=new Foo; pf->do_mesomething(); //using shared_ptr's overloaded -> (*pf).show_x();//using shared_ptr's overloaded *}//pf's destructor called here, destroying Foo

shared_ptr defines a conversion operator to bool. This is useful for testing whether the shared_ptr is empty. To access the raw pointer directly, call get():

if (pf) { //ensure that pf isn't empty before calling get Foo *palias = pf.get();}

The second shared_ptr constructor takes a pointer to a dynamically allocated object and an optional deleter. The following example defines a deleter and passes its address to shared_ptr’s constructor:

void my_deleter(Foo *ptr){ delete ptr; std::cout pf (new Foo, my_deleter); 

A deleter can be any callable entity: a free function, a function object, etc. When a user-defined deleter d is provided, shared_ptr‘s destructor calls:

d(ptr);

Instead of deleting ptr directly. A custom deleter enables you to bind shared_ptr to a pointer that was allocated in a different DLL, for example.

Certain resources such as file descriptors, synchronization objects, and device contexts require that a special API function be called to release them. Using a custom deleter, shared_ptr can manage such resources elegantly. In the following example, shared_ptr holds a mutex pointer obtained from an API function. When the shared_ptr object is destroyed, the release_mutex() function is called automatically:

//API functions for acquiring and releasing a mutexmutex_t* get_mutex(pid_t pid);int release_mutex(mutex_t* pmx);shared_ptr  pmutex (get_mutex(mypid),                                            release_mutex);

Counting on Reference Counting
Each shared_ptr destructor call decrements the bound pointer’s reference count. When the reference count reaches 0, the pointer is deleted. This property enables you to combine shared_ptr with STL containers and algorithms. The following example of creating a heterogeneous container is based on my September 2000 10-Minute Solution. This version however uses shared_ptr instead of bare pointers.

First, review the class hierarchy:

class mutimedia_file {public: explicit mutimedia_file(const string& filename); virtual ~mutimedia_file(); int virtual play();//..};class wav_file : public mutimedia_file;class mp3_file : public mutimedia_file;class ogg_file : public mutimedia_file;

Here’s how you populate the vector with heterogeneous shared_ptrs:

typedef std::tr1::shared_ptr  Pmm;typedef std::vector   Vpmm;void fill_and_play(Vpmm & v) { Pmm temp(new mp3_file("crazy4u"));//#1 create shared_ptr v.push_back(temp);//#2 store a copy of temp in v    //reuse temp  temp.reset(new wav_file("email_alert")); #3 v.push_back(temp); // insert shared_ptr to v v[0]->play(); // mp3_file::play() v[1]->play(); // wav_file::play());

Notice how fill() recycles the same shared_ptr object by calling the reset() member function. temp.reset(p2) causes temp to replace its existing pointer p with p2 and decrement p‘s reference count. Because a copy of the original shared_ptr has already been stored in the vector (line #2), p‘s reference count is higher than 0, and therefore the reset() call doesn’t delete the original pointer allocated in line #1. When fill() exits, temp is destroyed but the shared_ptrs stored in v keep the reference count of all pointers above 0. These pointers are deleted only when v itself is destroyed.

As far as heterogeneity is concerned, the code works correctly because assigning shared_ptr or T* to shared_ptr is perfectly legal as long as there is an implicit conversion from T* to U*. Furthermore, you can use a shared_ptr to store any pointer type in the container since shared_ptr‘s constructor is templated on the argument’s type:

shared_ptr  pf(new Foo);//fine

When pf is destroyed, it invokes Foo‘s destructor, as expected.

Pimpl Revisited
shared_ptr can eliminate the hassles of manual memory management when implementing the Pimpl idiom, which I presented here last month. In the header file containing the class’ declaration, replace the opaque pointer member with a shared_ptr (the revised code is highlighted):

//++file string.h#include #include //for shared_ptrclass String{public: String (); ~String(); //... size_t length() const; ostream & operator  shared_ptr  pimpl; //instead of opaque ptr };In the .cpp file, remove the destructor://++file string.cpp#include #include "Lock.h"#include "String.h"struct String::StringImpl{ vector  vc; size_t len; Lock lck;};String::String(): pimpl (new String::StringImpl) {}

That’s all!

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist