ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


Smart Pointers in C++

by Julio M. Merino Vidal
05/04/2006

C++, with its complex and complete syntax, is a very versatile language. Because it supports object-oriented capabilities and has powerful object libraries--such as the STL or Boost--one can quickly implement robust, high-level systems. On the other hand, thanks to its C roots, C++ allows the implementation of very low-level code. This has advantages but also carries some disadvantages, especially when one attempts to write high-level applications.

In this article I describe some common pitfalls that appear when manually managing dynamic memory in C++. This leads me to analyze which possible alternatives exist to avoid them, RAII-modeled classes being a good example. And finally, I present smart pointers and a description of some popular ones.

Memory Management

One of the things that C++ inherited from C is its memory management, which C leaves completely as the developer's responsibility. C++ wraps the malloc and free functions inside the new and delete operators, which are otherwise the same. This is good for simplicity and is enough in many scenarios, but it gets in the way when implementing large and high-level applications: memory management is a delicate, tedious, and difficult task, driving the developer away from the real problem at hand. Any bugs in this code tend to be fatal, crashing the program or making it unreliable for long-term usage.

Why is it so complex? After all, C developers have done it for ages. True--but look at how many bugs originated in memory mishandling. This task also becomes more complex in C++ due to exceptions. Why is memory management so delicate in C++?

Of course, if you opt to manage memory manually, you can work around these problems by being extremely careful during development. (Note, though, that this extra care means more development time.) In some situations this is the only way to go, because you need full control over what the program is doing (for example, when writing low-level code).

Then there is the complete opposite solution, very well known to Java and C# developers: using a (third party) garbage collector for C++. To my knowledge, these are seldom used in C++ even when writing very high-level applications, no matter what advantages they bring. This brings up another question: why aren't garbage collectors used more often in C++ code?

These three reasons, among others, may be why many developers choose not to use a garbage collector. For me, they are more than enough to look for another solution, especially because I don't like to lose portability because of third-party libraries. Yet falling back to manual control is not the best solution either, because it easily leads to the bugs described earlier.

One of the possible alternatives is to use stack (local) objects rather than heap (dynamic) objects. This is the typical and recommended C++ coding style, not to mention that many APIs enforce it: they receive and return references to local objects instead of pointers to heap objects. However, this scheme is not always applicable; multiple scenarios require the use of dynamically allocated objects.

What can you do in those cases where dynamic memory is unavoidable? A wise choice is to apply the RAII programming model to dynamically allocated objects, which brings up the well-known smart pointers.

The RAII Model

RAII (resource acquisition is initialization) is a programming idiom intended to make code more robust. This model is restricted to object-oriented programming languages that have a predictable object destruction flow: where the developer could deduce, if he wanted to, the exact place at which an object's destructor is called. C++ is one such language, assuming you use no garbage collector.

The basic principle behind RAII is to wrap dynamic resources--those that are allocated at some point and deallocated some time later--inside thin classes that manage them in a transparent way. These classes have several characteristics:

By using RAII-modeled classes, the developer need not care about explicitly releasing the acquired resources: they will be freed automatically when the instance that manages them goes out of scope. It is important to note that these instances are almost always local variables (they live in the stack or within another object as an attribute); placing them in the heap (thus making them dynamic) makes no sense and could defeat the whole idea behind RAII.

As an example, consider the std::ofstream class, modeled using RAII. This class has a redundant close method, provided only for cleanliness so as to allow the developer to explicitly tell where the file ceases to be open. If this function never gets called, the file will close as soon as the class's instance goes out of scope. For example:

int
read_something(void)
{
    int value;

    std::ifstream f("test.tmp");
    if (!f) {
        // First exit point
        throw no_such_file_exception("The file test.tmp does " +
                                     "not exist");
    }

    f >> value;
    if (value == 0) {
        // Second exit point
        throw invalid_format_exception("The file test.tmp is " +
                                       "corrupt");
    }

    // Third exit point
    return value;
}

The read_something function has at least three exit points, clearly marked in the code using comments. Now notice that there is no difference in f's handling in them. It does not matter whether the object was initialized, or if the code exited prematurely or successfully. The code simply ignores that the object exists, which is possible because std::ifstream's destructor closes the file. Otherwise, the code shown could leak an open file each time it took the second exit point.

For more information on the RAII programming idiom, I suggest you read The RAII Programming Idiom by Jon Hanna; it explains what RAII is in great detail and shows examples in both C++ and VB6.

Smart Pointers

A smart pointer is an RAII-modeled class that holds and manages a dynamically allocated chunk of memory. The exact behavior of the smart pointer is defined by its implementation, not by the fact that it is a smart pointer. In other words, a smart pointer is "smarter" than a raw pointer because it does some tasks automatically on behalf of the user; the tasks it does, however, depend on its specification.

Smart pointers are also helpful to specify how a pointer behaves based solely on notation. This is very useful when defining an API, because pointers in function signatures typically lead to confusion: Is the pointer an output parameter? Who is in charge of allocating the memory? Who has to release it? Using a smart pointer automatically answers some--if not all--of these questions.

The standard C++ library has a simple smart pointer, useful in many simple situations. However, due to those limitations, third-party libraries provide a wide variety of smart pointers with different features.

Smart Pointers in the Standard C++ Library

The standard C++ library comes with a smart pointer called auto_ptr, for "automatic pointer." It is very simple and is the least common denominator of smart pointers. No matter what, it is useful to make code more robust in many simple situations.

The auto_ptr smart pointer owns the object it holds a pointer to. That is, it releases the memory associated to it upon destruction; it does not do any allocation by itself, nor does it keep a reference counter of the memory involved.

The auto_ptr class overloads the * and -> operators so as to allow transparent access to the dynamic object. To access the raw pointer itself, use the get method. Consider a simple example:

std::auto_ptr<int> ptra(new int(5));

// From now on, the code is safe.  The dynamically allocated
// integer will be released no matter which execution path we
// follow.

*ptra = 4;
std::cout << *ptra << std::endl;
int* ptrb = ptra.get();
*ptrb = 3;
std::cout << *ptra << ", " << *ptrb
          << std::endl;

It is important to note in the example above that auto_ptr's constructor cannot fail. (It is declared as throws().) If it could, the code would need to be more complex in order to catch those failures, defeating in part the purpose of the smart pointer.

The automatic pointer also provides the release method, which detaches itself from the memory object, returning a raw pointer to it. This allows the use of an automatic pointer in a critical section only, falling back to manual management once that delicate code is over. As an example, imagine a function that returns a raw pointer to a dynamically allocated object. This function needs to do multiple initialization tasks and has several exit points if errors happen. You can use an automatic pointer to simplify its code:

foo *
get_new_foo(void)
{
    std::auto_ptr<foo> ob(new foo);

    // The following two operations can raise an exception.
    // We need not care about it thanks to the smart pointer.
    ob->do_something();
    ob->do_something_else();

    // We are done.  The caller is only interested in the raw
    // pointer, which we can safely return now.
    return ob.release();
}

Automatic pointers are not copyable. If the developer attempts to copy an instance of auto_ptr, the object it points to will be transferred to the new smart pointer, invalidating the old one. Therefore, using this class together with STL collections is dangerous; don't do it, because it does not follow the required semantics. Furthermore, if two automatic pointers hold a reference to the same memory object, the behavior is undefined (but typically the application will simply crash).

std::auto_ptr<int> ptra(new int(5));

// ptra is valid and can be accessed.
*ptra = 4;

std::auto_ptr<int> ptrb(ptra);

// ptrb now owns the dynamically allocated integer, so we can
// access it.
*ptrb = 3;

// However, ptra is now longer valid; the following crashes the
// program.
*ptra = 2;

// At last, the following has undefined behavior.
int* i = new int(1);
std::auto_ptr<int> ptrc(i);
std::auto_ptr<int> ptrd(i);

To conclude this section, let me add another warning: be aware that you cannot use auto_ptr to hold a reference to an array, because it will not be released properly (that is, delete vs. delete[]). If you need to do this, you have multiple alternatives: wrap the array in some other class of your own, use std::vector instead of an array, or use another smart pointer.

Smart Pointers in Boost

Due to the inherent limitations of the standard auto_ptr class, several third-party C++ libraries provide enhanced smart pointers. Boost is no exception. For those who do not know it, Boost is a collection of free, portable, peer-reviewed C++ libraries. These libraries work well with the standard C++ library, following its style and design principles. Some of them may eventually become part of the C++ standard.

The Boost Smart Pointers library provides six smart pointer templates in three groups: scoped pointers, shared pointers, and intrusive pointers.

Given that the official documentation for the Boost Smart Pointers library is very informative and complete, I have focused the rest of this article on presenting each available smart pointer accompanied by some code examples. If you need to learn more about them, I encourage you to read the official information.

Scoped pointers

A scoped pointer owns a dynamically allocated object, much like the standard auto_ptr does. The difference lies in that the scoped pointer is explicitly marked as noncopyable, ensuring that during compilation time the pointer is never copied. It can be seen as a way to give a casual reader of your code a signal that you do not intend for that specific object to leave the local scope (hence the name "scoped pointer").

Boost provides two scoped pointers: scoped_ptr and scoped_array. The former is good for raw pointers, while the latter is useful for dynamic arrays (those allocated with new[]) because they are treated in a slightly different manner. These two smart pointers are extremely lightweight: they do not occupy more space than a raw pointer, and the most common operations are as cheap as the access to a raw pointer.

Here is an example to illustrate the usage of both classes by dynamically allocating a test class and letting its instances go out of scope. The printed messages show how the code properly releases the memory before exiting:

#include <cstdlib>
#include <iostream>

#include <boost/scoped_array.hpp>
#include <boost/scoped_ptr.hpp>

static int count = 0;

class printer
{
    int m_id;

public:
    printer(void) :
        m_id(count++)
    {
    }

    ~printer(void)
    {
        std::cout << "Printer " << m_id
                  << " destroyed" << std::endl;
    }
};

int
main(void)
{
    boost::scoped_ptr<printer> p1(new printer);
    boost::scoped_array<printer> p2(new printer[5]);

    std::cout << "Exiting test program" << std::endl;

    return EXIT_SUCCESS;
}

Shared pointers

A shared pointer is something you cannot live without once you've learned what it is. If smart pointers are great by themselves, imagine how incredible these are! Really. Remember the essay about garbage collectors at the beginning of the article? Shared pointers are the alternative I was aiming at.

More seriously, a shared pointer owns a reference to a dynamically allocated pointer--as all other smart pointers do--but does not own the object itself. This subtle difference is very important: a shared pointer counts the number of users of the dynamic object by means of a reference counter. The dynamic object is released only when the last shared pointer referencing it disappears.

You can copy and transfer shared pointers in a trivial way, yet these operations are completely safe: they do not duplicate memory nor try to release a single chunk earlier than necessary. Even more, they are also useful to write clearer code. By using a shared pointer, you no longer need to identify who is responsible for freeing a returned object nor which rules apply to an input parameter. This certainly helps when designing a public API.

Boost provides two shared pointers: shared_ptr and shared_array. The former is used for raw pointers, while the latter is used for dynamic arrays (those allocated with new[]), for the same reasons as scoped pointers.

Consider a simple example:

#include <cstdio>
#include <iostream>

#include <boost/shared_array.hpp>

class useless_buffer
{
    size_t m_length;
    boost::shared_array<char> m_buffer;

public:
    useless_buffer(const std::string& str) :
        m_length(str.length() * 2),
        m_buffer(new char[m_length])
    {
        std::strcpy(m_buffer.get(), str.c_str());
    }

    boost::shared_array<char> get_buffer(void)
    {
        return m_buffer;
    }

    boost::shared_array<char> copy_buffer(void)
    {
        boost::shared_array<char> copy(new char[m_length]);
        std::memcpy(copy.get(), m_buffer.get(), m_length);
        return copy;
    }
};

int
main(void)
{
    useless_buffer buf("Hello, world!");

    std::cout << buf.get_buffer().get() << std::endl;
    std::cout << buf.copy_buffer().get() << std::endl;

    return EXIT_SUCCESS;
}

It is interesting to note in this example that from the caller's point of view, there is absolutely no visible difference between the get_buffer and copy_buffer methods. The former returns a new reference for a local array, which the caller must not release. The latter returns a new dynamic object, which the caller must release.

As a complement to shared pointers, Boost provides the weak_ptr smart pointer. This one does not own an object but instead holds a "weak reference" to an existing shared pointer. The shared pointer can change at will regardless of how many weak pointers are "watching" it.

More in detail, a weak pointer is associated to a shared pointer. Whenever someone needs to access the contents of a weak pointer, the weak pointer must first be converted to a shared pointer by means of the lock method or a special constructor. If the object managed by the shared pointer goes away, any further conversion from the weak pointer to a shared pointer will fail.

Here is a fictitious example in which the use of a weak pointer might be useful. A shared pointer is accessed by two threads. thread1 should be able to release the object, but the object should be accessible at some point in thread2 only if it still exists. Using the weak pointer makes this situation possible without using a complex locking protocol:

boost::shared_ptr<int> ptri(new int(5));

void
thread1(void* arg)
{
    ...

    ptri.reset();

    ...
}

void
thread2(void* arg)
{
    boost::weak_ptr<int> wptr(ptri);

    ...

    if (boost::shared_ptr<int> auxptr = ptri.lock()) {
        // Do something with 'auxptr' because the object is
        // still available.
    }

    ...
}

Intrusive pointers

The intrusive pointer is a light version of the shared pointer. It assumes that the dynamically allocated object it has to point to implements a reference counter by itself. This maintains the counter in a single place and keeps the smart pointer to the minimum size (the size of the raw pointer).

In order to define an intrusive pointer for a given object, you must first create two functions that manage its embedded reference counter. One is intrusive_ptr_add_ref, which simply increases the counter; the other is intrusive_ptr_release, which decreases the counter and, if it reaches zero, releases the object.

This is much clearer with an example. You may want to assume that some_resource is a class you have written or a class provided by someone else, perhaps even the operating system itself.

#include <cstdlib>
#include <iostream>

#include <boost/intrusive_ptr.hpp>

class some_resource
{
    size_t m_counter;

public:
    some_resource(void) :
        m_counter(0)
    {
        std::cout << "Resource created" << std::endl;
    }

    ~some_resource(void)
    {
        std::cout << "Resource destroyed" << std::endl;
    }

    size_t refcnt(void)
    {
        return m_counter;
    }

    void ref(void)
    {
        m_counter++;
    }

    void unref(void)
    {
        m_counter--;
    }
};

void
intrusive_ptr_add_ref(some_resource* r)
{
    r->ref();
    std::cout << "Resource referenced: " << r->refcnt()
              << std::endl;
}

void
intrusive_ptr_release(some_resource* r)
{
    r->unref();
    std::cout << "Resource unreferenced: " << r->refcnt()
              << std::endl;
    if (r->refcnt() == 0)
        delete r;
}

int
main(void)
{
    boost::intrusive_ptr<some_resource> r(new some_resource);
    boost::intrusive_ptr<some_resource> r2(r);

    std::cout << "Program exiting" << std::endl;

    return EXIT_SUCCESS;
}

Conclusion

I discovered smart pointers a year ago, and since then I firmly believe that their use makes code safer, more robust, and easier to read. I can no longer conceive any C++ code that directly manages dynamically allocated objects due to all the problems that can arise.

Now that you have finished this article, I hope you feel similarly and that from now on you will use these techniques to improve your own programs.

Time to code!

Julio M. Merino Vidal studies computer science at the FIB faculty in Barcelona, Spain.


Return to the ONLamp.com.

Copyright © 2009 O'Reilly Media, Inc.