Using Smart Pointers in C++

2010 April 19

This article gives an overview of a bunch of smart pointer classes for C++. Some of the facilities discussed are non-standard, but part of widely used libraries. First let us define a class that will serve as the base of all our code examples. The class is called FileReader. It wraps up the low-level details of opening and reading a text file. The constructor of the class is passed the name of a file. The file is opened and its handle is stored in a private member file_ of type FILE. The destructor will close the FILE handle, thereby preventing any scope for resource leaks. The user can call the read(string&) function to get the contents of the file.

class FileReader
{
public:
  FileReader () : file_name_(""), file_(NULL)
  { }

  FileReader (const std::string &file_name)
    : file_name_(file_name), file_(NULL)
  { 
    file_ = fopen (file_name_.c_str(), "r"); 
    if (file_ == NULL)
      throw std::string ("file_open_failed");
  }

  ~FileReader ()
  {
    if (file_ != NULL)
      {
        fclose (file_);
        file_ = NULL;
      }
  }

  void read (std::string &data)
  {
    if (file_ == NULL) 
      return;
    size_t sz = get_file_size ();
    char *buffer = new char [sz + 1];
    fread (buffer, sizeof (char), sz, file_);
    buffer[sz] = '\0';
    data.assign (buffer);
    delete[] buffer;
  }

private:
  size_t get_file_size ()
  {
    fseek (file_, 0L, SEEK_END);
    size_t sz = ftell (file_);
    fseek (file_, 0L, SEEK_SET);
    return sz;
  }

  std::string file_name_;
  FILE *file_;
};
This class makes use of RAII and takes care of its own memory management:

void test (const std::string &fileName)
{
    FileReader fileReader (fileName);
    std::string contents;
    fileReader.read (contents);
} // The destructor is called and the file_ handle 
  // is released at the end of scope.

If we create FileReader objects dynamically, then we have to explicitly call delete to execute the destructor:

void test (const std::string &fileName)
{
    FileReader *fileReader = new FileReader
                                  (fileName);
    std::string contents;
    fileReader->read (contents);
    delete fileReader;
} 
Standard C++ gives us auto_ptr, a smart pointer facility which takes ownership of a dynamic object and cleans it up automatically when the auto_ptr goes out of scope. We can re-write our test function to use auto_ptr:

#include <memory> // for auto_ptr.

void test (const std::string &fileName)
{
    std::auto_ptr<FileReader> fileReader (new
                               FileReader (fileName));
    std::string contents;
    fileReader->read (contents);
} // FileReader pointer is destroyed by auto_ptr.
auto_ptr has one associated danger that we need to be aware of. One pointer can be owned only by a single auto_ptr at a time. So when we copy an auto_ptr the ownership is transfered to the destination auto_ptr and the pointer in the source auto_ptr is set to NULL. This is required to avoid releasing the wrapped-up pointer more than once. Thus the call to read() in the following function will lead to unspecified behavior:

void test (const std::string &fileName)
{
    std::auto_ptr<FileReader> fileReader (new
                           FileReader (fileName));    
    std::string contents;
    std::auto_ptr<FileReader> tmp = fileReader;
    // Unspecified behavior, probably a crash!
    fileReader->read (contents); 
} 

The Boost library provide an auto_ptr replacement that don't have this problem. This class is called scoped_ptr. A scoped_ptr is noncopyable and prevents shared ownership of a pointer. The attempt to make a copy of a scoped_ptr will result in a compile-time error:

#include <boost/scoped_ptr.hpp>

void test (const std::string &fileName)
{
    boost::scoped_ptr<FileReader> fileReader (
                             new FileReader (fileName));    
    // Compile time error.
    std::string contents;
    boost::scoped_ptr<FileReader> tmp = fileReader; 
    fileReader->read (contents); 
} 
This prevents scoped_ptr objects from being used in containers like std::vector as they need to make copies of the object. If you want a smart pointer that is copyable, use a shared_ptr. It uses reference counting and memory is reclaimed when the last shared_ptr holding a reference to it goes out of scope. shared_ptr also provide comparison operators so that it can work with the standard associative containers.

#include <boost/shared_ptr.hpp>

void test (const std::string &fileName)
{
    boost::scoped_ptr<FileReader> copy;
    {
       boost::scoped_ptr<FileReader> fileReader (
                                  new FileReader (fileName));    
       std::string contents;
       tmp = fileReader;
       fileReader->read (contents); 
    } // The FileReader object will not be reclaimed
      // here as copy holds a reference to it. 
} // The FileReader object is reclaimed when copy goes 
  // out of scope.

Neither scoped_ptr nor shared_ptr can manage arrays as they use delete to release the pointer. Use scoped_array or shared_array, which make use of delete[] for that purpose. The following sample shows allocating an array of integers using scoped_array:

void test ()
{
  size_t count = 10;
  boost::scoped_array<int> squares (new int[count]);
  for (size_t i = 0; i < count; ++i)
    {
      squares[i] = i * i;
    }
  for (size_t i = 0; i < count; ++i)
    {
      std::cout squares [i] ' '; 
      // => 0 1 4 9 16 25 36 49 64 81 
    }
}