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
}
}