Dynamic C++ arrays using STL

by John Miano

C++ís treatment of arrays is awkward in many ways. Most of the problems can be traced back to Cís quirky interchangeability of arrays and pointers. This article will explain how to implement dynamic arrays using the Standard Template Library (STL) vector and auto_ptr classes.

Defining the problem

Unlike some programming languages, C++ does not support dynamic arrays. In many applications there is a need for arrays whose sizes are set by function parameters. Unfortunately, a declaration such as this one is illegal because array bounds must be a constant expression:

void SomeFunction(int size)
{
  int array[size]; // ILLEGAL
  // . . .
  return;
}

If you need to use a parameter to specify the size of an array, you can use new to allocate the array at the start of a function and use delete to free it at the end of the function:

void SomeFunction(int size)
{
  int *array = new int[size]; 
  // . . .
  delete[] array;
  return;
}

The problem with this approach is that it is not completely reliable when it comes to preventing memory leaks. The memory allocated by new will not get freed if an exception occurs before the call to delete.

To ensure that all allocated memory is freed, we need to create an exception handler within the function. Since exception handling in C++ does not include a finally construct we need to use two delete calls. One is called when an exception occurs, the other when there are no exceptions. For example:

void SomeFunction(int size)
{
  int *array = new int[size]; 
  try
  {
    // . . .
  }
  catch(...)
  {
    delete[] array;
    throw;
  }
  delete[] array;
  return;
}

The previous example works when you have one array, but what if you need more? Here we add a second array using the same error handling process:

void SomeFunction(int size)
{
  int *array1 = new int[size]; 
  double *array2 = new double[size];
  try
  {
    // . . .
  }
  catch(...)
  {
    delete[] array1;
    delete[] array2; 
  }
  delete[] array1;
  delete[] array2;
  return;
}

Adding the second array introduces the potential for a new memory leak. If the second new call fails it will throw a bad_alloc exception. Since the new calls are outside of the exception handler, the memory allocated by the first new call will not be freed if the second call fails.

We could try to close the potential memory leak by moving the new calls within the try block:

void SomeFunction(int size)
{
  int *array1; 
  double *array2; 
  try
  {
    array1 = new int[size]; 
    array2 = new double[size]; 
    // . . .
  }
  catch(...)
  {
    delete[] array1;
    delete[] array2; // Wrong
  }
  delete[] array1;
  delete[] array2;
  return;
}

Unfortunately, this introduces a subtle error. If one of the new calls fails and throws a bad_alloc exception, at least one and possibly both of the pointers will not be initialized. The delete call using the initialized pointer in the exception handler will produce unpredictable and usually catastrophic results.

In order to ensure that the delete calls will work whether or not the new calls succeed, we need to initialize the pointers to null before the first call. This example allocates two arrays in a safe manner:

void SomeFunction(int size)
{
  int *array1 = 0; 
  double *array2 = 0;
  try 
  {
    array1 = new int[size]; 
    array2 = new double[size]; 
    // . . .
  }
  catch(...)
  {
    delete[] array1;
    delete[] array2;
    throw;
  }
  delete[] array1;
  delete[] array2;
  return;
}

The goal here is to create a dynamic array, but most of the code is for handling errors that may not even occur.

The obvious way to avoid all of this error handling code would be to create an array class with an overloaded [] operator. Since an auto objectís destructor is always called when the function exits, even as the result of an exception, using an array class could ensure that that all of the dynamic memory would be freed. If we made the array class a template, we would even be able to use the same code to handle int and double arrays.

The vector class

The best solution to dynamic arrays is so obvious once you think about it that you might wonder if anyone had thought of it before. Not only did people think of it, they included it in the STL as defined by the ANSI C++ standard. The solution is the vector template class.

Using the vector class, the previous example looks like this:

#include <vector>
void SomeFunction(int size)
{
  std::vector<int> array1(size); 
  std::vector<double> array2(size); 
  // . . .
  return;
}

This example is even simpler than using new and delete with no error handling, and has the added benefit of being much more readable.

Initialization

Another advantage of the vector class over new and delete is that you can specify an initial value for all of the elements of the array. The second parameter in the constructor specifies the initial value for all elements in the array so this

void SomeFunction(int size)
{
  std::vector<int> array1(size, (int)-1);
  std::vector<double> array2(size, 0.0); 
  // . . .
  return;
}

is equivalent to this

void SomeFunction(int size)
{
  std::vector<int> array1(size); 
  for (int ii = 0; ii < size; ++ii)
    array1[ii] = -1;
  std::vector<double> array2(size, 0.0);
  for (int ii = 0; ii < size; ++ii)     
    array2[ii] = 0;
  // . . .
  return;
}

Range Checking

As with normal arrays, the vector classís [] operator does not perform range checking. While this makes the vector class no less efficient than a C++ array, it makes applications more susceptible to errors.

The at() member function allows you to do range checking with a vector object. at() throws an out_of_range exception when the index is not within the bounds of the array. The at() function returns a reference so you can use it to make assignments:

std::vector<int> array(10);
array[10] = 42; // Bad but no exception
array.at(10) = 42; // Exception

You might want to use range checking while debugging and have it disabled at other times for performance. An easy way to do this is to create your own class derived from vector. The class shown in Listing A called, Array, allows you to enable range checking by defining the preprocessor symbol ENABLECHECKS. You can use the Array class as a substitute for the vector class.

The auto_ptr class

While arrays are the most common example of dynamic memory intended to last for a single function call, the problem of ensuring that memory allocated in a function is freed applies to other objects as well.

The auto_ptr template class in the standard C++ library provides a simple mechanism to ensure that dynamically allocated objects are deleted when a function exits. The auto_ptr class essentially makes objects allocated by new behave like an auto object.

The auto_ptr class maintains a pointer to an object allocated with new. The class defines overloaded * and -> operators that give access to the dynamic object. An auto_ptr variable assumes ownership of its related object. When the variable goes out of scope, the auto_ptrís destructor deletes the dynamic object.

The following example shows how to use an auto_ptr object to reference a dynamically allocated ofstream object. The auto_ptr object will delete the ofstream object even if an exception causes the function to exit. Hereís the code:

#include <fstream>
#include <memory>
using namespace std;
void SomeFunction(const string &filename)
{
  auto_ptr<ofstream> strm(
    new ofstream(filename.c_str()));
  if (*strm)
  {
    *strm << "Anyone home" << endl;
    strm->close();
  }

  // . . .
}

The auto_ptr class defines a copy constructor and assignment operator so that you can have more than one auto_ptr variable reference the same object. In addition to creating an additional reference to the same object, the auto_ptr class transfers ownership of the object whenever one auto_ptr variable is assigned to another, either by the copy constructor or assignment operator.

In order to prevent multiple auto_ptr variables from deleting the same dynamic object, auto_ptr only deletes the associated dynamic object if it owns the object. Multiple auto_ptr variables can reference the same object but only one can own it.

In this example, both strm1 and strm2 can be used to access the same ofstream object, but only the owner of the object, strm2, will delete it when the function exits:

#include <memory>
#include <fstream>
using namespace std;
void SomeFunction(const string &filename)
{
  auto_ptr<ofstream> strm1(
    new ofstream(filename.c_str()));
  auto_ptr<ofstream> strm2;
  strm2 = strm1; 
  // strm2 now owns the object.

  // . . . 
}

You need to be careful when assigning one auto_ptr variable to another in a different scope. In the following example, strm2 takes ownership of the ofstream object during the assignment. When strm2 goes out of scope the ofstream object gets deleted, at which point strm1 no longer references a valid object:

#include <memory>
#include <fstream>
using namespace std;
void SomeFunction(const string &filename)
{
  auto_ptr<ofstream> strm1(
    new ofstream(filename.c_str()));
  {
    auto_ptr<ofstream> strm2;
    strm2 = strm1;
  }
  *strm1 << 123 << endl; // ERROR

  // . . . 
}

Just as deadly is associating an auto, static, or extern object with an auto_ptr variable. While doing so is legal syntactically, it will produce unpredictable results as the auto_ptr attempts to delete the object when it goes out of scope:

#include <memory>
#include <fstream>
using namespace std;
void SomeFunction(const string &filename)
{
  ofstream strm(filename.c_str());
  auto_ptr<ofstream> strm1(&strm);
  // . . . 
}

Conclusion

The vector class in the standard template library is the best mechanism for creating dynamic arrays with one dimension. STL vector objects can be referenced exactly like an array allocated with new, but vector objects have the advantage that dynamic memory is managed for you.

The auto_ptr template class provides similar memory management capabilities for generic objects. Your code must allocate the dynamic object but auto_ptr handles the deletion.

!!Bob: Iíve left the margins on this listing wide as I expect this listing to appear on the back page where we have the entire page width to work with. - Kent

Listing A: The Array class.

template<class TYPE>
class Array : public std::vector<TYPE>
{
public:
  Array () {} 
  Array (size_t size) : vector<TYPE>(size) {} 
  Array (size_t size, const TYPE &value) : vector<TYPE>(size, value) {} 
  Array (const Array &source) : vector<TYPE>(source) {} 
  TYPE &operator[](size_t) ;
  const TYPE &operator[] (size_t) const ;
} ;

#if defined (ENABLECHECKS)

template<class TYPE>
inline TYPE &Array<TYPE>::operator[](size_t index)
{
  return vector<TYPE>::at (index) ;
}
template<class TYPE>
inline const TYPE &Array<TYPE>::operator[](size_t index) const
{
  return vector<TYPE>::at (index) ;
}

#else

template<class TYPE>
inline TYPE &Array<TYPE>::operator[](size_t index)
{
  return vector<TYPE>::operator[](index) ;
}
template<class TYPE>
inline const TYPE &Array<TYPE>::operator[](size_t index) const
{
  return vector<TYPE>::operator[](index) ;
}

#endif