STL Containers and Auto_ptrs - Why They Don't Mix

STL Containers and Auto_ptrs - Why They Don't Mix

I was actually asked a question around this topic in an interview a few years ago. It is quite an interesting area and delves into some deep parts of C++.

A common scenario in C++ is to create a container of user defined objects. In quantitative finance, this might be a vector of derivative contracts, for instance. One (bad) way to achieve this is by using a container of pointers to the option object.

Container of "Dumb" Pointers

#include <vector>

class Option {
public:
  ..
}

void vec_option(std::vector<Option*>& vec)
{
  vec.push_back(new Option());

  // .. Some additional code ..

  delete vec.back();    // Skipping this line causes a memory leak
  vec.pop_back();       // Causes a dangling pointer if this line isn't reached
}

If the code prior to delete vec.back; causes an exception to be thrown or returns from the function, this will lead to a memory leak or a dangling pointer. Why is this? Simply, because although the destructor called by std::vector, it does NOT remove the allocations that were made by the new operator, when creating the Option object.

Containers of Auto_ptrs

Although the above code will compile and run, it is bad practice to use such containers of "dumb" pointers due to the fragility of the function. The solution to such a problem is to use a smart pointer.

One such smart pointer is std::auto_ptr<>. Let's modify the function prototype code above to make use of it:

#include <vector>
#include <memory>   // Needed for std::auto_ptr<>

class Option {
public:
  ..
}

void vec_option(std::vector<std::auto_ptr<Option> >& vec)
{
  vec.push_back(new Option());
  ..
  vec.pop_back();
}

Now when we try to compile this code we receive a compiler error! What just happened?

It all comes down to the contract that std::auto_ptr<> makes with you when you agree to use it in your code.

std::auto_ptr<> does not fulfill the requirements of being copy-constructible and assignable. Unlike objects which do have this requirement, when copying or assigning an auto_ptr the two elements are not logically independent. This is because auto_ptr has semantics of strict ownership and is thus solely responsible for an object during the object's life cycle. If we copy the auto_ptr then the source auto_ptr will lose the reference to the underlying object.

Since objects within an STL container must be copy-constructible and assignable, a compile time error is provided if an auto_ptr is used within a container. Algorithms, such as those involved in sorting STL containers, often copy objects while carrying out their tasks. Hence, there would be a large scope for memory leaks and/or dangling pointers if containers of auto_ptrs were allowed.

Boost Smart Pointers and STL Containers

Prior to the C++11 standard the best way to overcome this problem was to use the Boost library smart pointers. In this instance we could use the Boost library shared pointer - boost:shared_ptr. A shared pointer is useful because it removes the possibility of a memory leak due to neglect of iterating over the vector and calling delete for each item. Let's modify the example above to make use of the shared pointer:

#include <vector>
#include <boost/shared_ptr.hpp>  // Need to include the Boost header for shared_ptr

class Option {
public:
  ..
}

typedef boost::shared_ptr<Option> option_ptr;   // This typedef stops excessive C++ syntax later

void vec_option(std::vector<option_ptr>& vec)
{
  option_ptr ptr_opt(new Option());  // Separate allocation to avoid problems if exceptions are thrown
  vec.push_back(ptr_opt);
  ..
  vec.pop_back();
}

So why does this work? Shared pointers make use of reference counting, which ensures that the allocation option object (ptr_opt) is correctly transferred into the vector, in this line: vec.push_back(ptr_opt);. Although it is not absolutely necessary to use a shared_ptr, I have done so here because debugging dangling pointers when using different types is extremely painful. It is much easier to optimise working code than trying to prematurely optimise non-working code!

C++11 Smart Pointers and STL Containers

In modern C++, which utilises the C++11 standard, the use of auto_ptrs has become deprecated. In addition, certain new smart pointers have made it into the standard. The Boost shared_ptr previously described was included via the TR1 standard to C++, then eventually made it into C++11, with some additional modifications. It is straightforward to modify the above Boost code to incorporate the new C++11 shared pointers:

#include <vector>
#include <memory>  // std::shared_ptr is included in the "memory" header

class Option {
public:
  ..
}

typedef std::shared_ptr<Option> option_ptr;   // This typedef stops excessive C++ syntax later

void vec_option(std::vector<option_ptr>& vec)
{
  option_ptr ptr_opt(new Option());  // Separate allocation to avoid problems if exceptions are thrown
  vec.push_back(ptr_opt);
  ..
  vec.pop_back();
}

One final note - using shared pointers can be considered bad practice, as it means that the programmer has not given sufficient thought to the lifetime of the object or where it should actually be deleted. Another suggestion is to use std::unique_ptr for situations where the container object "owns" the elements within it, rather than the elements having a distinct external lifetime of their own. I won't dwell on this too much here as it is really a discussion long enough for another article!