C++ Programming/Memory Management

Memory management is a large subject, and C++ offers a wide range of choices for how to manage memory (and other resources, but our focus will initially be on memory).

The good news is that modern C++ makes memory management straightforward in most cases, while providing comprehensive facilities for those who need to stray from the beaten path. We will cover both the high-level approaches (which are usually preferable), and also give details of lower-level aspects such as use of new/delete/new[]/delete[] which are usually best hidden inside classes implementing higher level patterns.

Garbage collection (GC) deals with the management of dynamic memory, with different levels of automation, where the construct called collector, attempts to reclaim garbage (memory that was used by application objects that will never be accessed or mutated again). This is often regarded as an important feature of recent languages, especially if they forbid manual memory management, that is very prone to errors and therefore requires a high level of experience from programmers. Errors due to memory management result mostly in instabilities and crashes that are only noticed at runtime, making them extremely hard to detect and correct.

C++ has optional support for garbage collection and some implementations include garbage collection (often based on the so-called Boehm collector) do exist. The C++ standard defines the implementation of the language and it's underlining platform opening it for the inclusion of extensions, for instance Sun's C++ compiler product does include the libgc library (a conservative garbage collector).

Unlike many high level languages, C++ does not impose the use of garbage collection, and mainstream C++ idioms for memory management do not assume the use of conventional automated garbage collection. The most common garbage collection method in C++ is the use of the strangely named idiom "RAII", that stands for "Resource Acquisition Is Initialization", this idiom is covered in the RAII Section of the book. The key idea behind RAII is that a resource, whether acquired at initialization time or not, is owned by an object, and that the object's destructor will automate the release of that resource at an appropriate time. This enables C++ through RAII to support deterministic cleanup of resources, since the same approaches that work for freeing memory can also be used to release other resources (file handles, mutexes, database connections, transactions, and many more).

In the absence of a default garbage collection, RAII is a robust way to ensure that resources are not leaked even in code that might cause exceptions to be thrown. It is arguably superior to the finally construct in Java and similar languages; when a class owns a resource, Java requires every user of that class to wrap its uses in a try/finally block. In C++ the class provides a destructor, and users of that class don't need to do anything except ensure that the object is destroyed when they are finished with it (which normally takes no work, for example in the case that the object is a local variable or a data member of another object).

For common applications, the appropriate classes have already been written: many simple cases of memory management are covered by std::string and std::vector (along with the other standard containers such as std::map and std::list).

The C++ version is shorter and doesn't contain any explicit code to work out how much memory to allocate, to allocate or free memory and doesn't need to know the implementation details of 'getstr()'; that's all taken care of by the standard string class. The C++ version also traps failure of memory allocation, whereas the C version shown above needs additional checking on the result of realloc in order to be safe in low-memory situations.

While smart pointers have many more uses in C++ than simple memory management, they are often useful ways to manage the lifetimes of other dynamically allocated objects.

A smart pointer type is defined as any class type that overloads operator->, operator*, or operator->*. One thing to note straight away is that "smart pointers" are, in a sense, not really pointers at all -- but overloading these operators allows a smart pointer to behave much like a built-in pointer, and much code can be written which works with both "real" pointers and smart pointers.

The only smart pointer type included in the 2003 C++ Standard is std::auto_ptr. While this has certain uses, it is not the most elegant or capable of smart pointer designs.

Provides the ability to:

simulate the lifetime of a local variable or member variable for an object that is actually dynamically allocated

provide a mechanism for "transfer of ownership" of objects from one owner to another.

Simple auto_ptr example

#include <memory> // for std::auto_ptr#include <iostream>class Simple {public:
std::auto_ptr<int> theInt;
Simple(): theInt(newint()){*theInt =3;//get object like normal pointer}int f(){return42;}// when this class is destroyed, theInt will// automatically be freed};int main(){
std::auto_ptr<Simple> simple(new Simple());// note that the following won't work:// std::auto_ptr<Simple> simple = new Simple();// as auto_ptr can only be constructed with new values// access member functions like normal pointers
std::cout<< simple->f();// the Simple object is freed when simple goes out of scopereturn0;}

The = operator in auto_ptr works in a different to normal way. What it does is transfers ownership from the rhs (right hand side) auto_ptr to the lhs (left hand side) auto_ptr. The rhs pointer will then point to NULL.

This behavior is useful when it is desired that only one pointer ever points to a particular object, but the pointer that does point to it may be changed. If different behavior is desired, using one of the boost pointers is a better option.

To do:

Give examples of same use of auto_ptr.(partly done)

Warn against using auto_ptr naively for data members in classes. Include an example of unsafe use, and show how to make it safe. Maybe refer to the rule of three (in this case, we need to do something special for copy operations but not for the destructor.

Note that auto_ptr must not be used with incomplete types, hence isn't useful for pimpl etc.

note that --- The way that auto_ptr transfers ownership is rather odd, and widely disapproved of, but with care it can be used effectively.

The boost c++ libraries include 5 different kind of smarts pointers with, along with the std::auto_ptr, can be used in almost all memory management situations. Also, some of the smart pointers in boost are going to be in the standard libraries for the proposed c++0x revision of c++ when it is released.

The boost and std smart pointers

Pointer

Usage situation

Performance cost

Transfer of ownership

Sharing objects

Works with

Other

std::auto_ptr

An object can only be owned by one auto_ptr at a given time, this owner may be changed though

nil

Yes

No

Single instance

Doesn't work with standard containers (std::vector etc.)

boost::scoped_ptr

An object is assigned to a scoped_ptr, it can never be assigned to another pointer

Nil

No

No

Single instance

If used as a member of a class, must be assigned in the constructor. Also, doesn't work with standard containers (std::vector etc.)

boost::shared_ptr

Many shared_ptrs may point to a single object, when all go out of scope, the object is destroyed

Yes, uses reference counting

Yes

Yes

Single instance

Works with standard containers

boost::weak_ptr

used with shared_ptrs to break possible cycles, which may result in memory leaks. To use, must be converted into a shared_ptr

same as shared_ptr

Yes

Yes

Single instance

Only ever used in conjunction with shared_ptrs

boost::scoped_array

same as scoped_ptr, but works with arrays

Nil

No

No

Array of instances

See scoped_ptr

boost::shared_array

Same as shared_ptr, but works with arrays

Yes, uses reference counting

Yes

Yes

Array of instances

See shared_ptr

boost::intrusive_ptr

Used to create custom smart pointers for objects that have their own reference count

One of the rationale of using smart pointer is to avoid leaking memory. In order to avoid this, we should avoid manually managing heap-base memory. So, we have to find a container which can automatically return the memory back to the operation system when we do not use it. The destructor of class can match this requirement.

What we need to store in a basic smart pointer is, of course, the address of the allocated memory. For this, we can simply use a pointer. Let's say we are designing for storing a piece of memory for an int.

class smt_ptr
{
private:
int* ptr;
};

In order to make sure that every user puts an address in this smart pointer when doing initialization, we have to specify the constructor to accept a declaration of this smart pointer with the target address as the argument, but not "mere declaration" of the smart pointer itself.

We have to allow users to access the data stored in this smart pointer and make it more "pointer-like". For this, we may add a function to provide the access the raw pointer, and overload some operators, such as operator* and operator->, to make it behave like a real pointer.

This implementation is really very basic and only provides basic features, and is subject to many serious problems, such as copying this smart pointer will lead to double deletion but we are not discussing these problems here.

Apart from auto_ptr, there are many other smart pointers to cover tasks from wrapping COM objects, providing automatic synchronization for multi-threaded access, or providing transaction management for database interfaces.

A good repository for many of these is the Boost library; some smart pointers from boost are included in the C++ Committee's "TR1", a collection of library components which integrate well with standard C++.

Modern C++ code tends to use new quite rarely, and delete very rarely. From a memory standpoint, the disadvantage is that "new" allocates memory off the heap while local objects allocate memory off the stack. Heap allocation times are much slower than allocations off the stack. However, there are still times when it's appropriate to do so, and a solid understanding of how these low-level facilities work can help with understanding of what normally happens "below the hood". There are even times when new and delete are too high-level, and we need to drop back to malloc and free -- but those situations are rare exceptions indeed.

The basic idea of new and delete is simple: new creates an object of a given type and gives a pointer to it, and delete destroys an object created by new, given a pointer to it. The reason that new and delete exist in the language is that code often does not know when it is compiled exactly which objects it will need to create at runtime, or how many of them. Thus new and delete expressions allow for "dynamic" allocation of objects.

Unfortunately it is hard to write a realistic example in a few lines of code; dynamic allocation is only justified when simpler approach don't work, for example because an object needs to outlive a function's scope, or because it uses so much memory that we only want to create it on demand.

To do:
It would be nice for the paragraph below that compared new/delete with malloc/free to be in some kind of side-bar or otherwise set off from the main text, as it's primarily of interest to those with a background in C. I don't know what would be good markup to use to achieve that.

For those of you familiar with the C programming language, new is a kind of "type-aware" version of malloc: the type of the expression "new int" is "int*". Hence in C++ where a cast would be necessary to write int * p = reinterpret_cast<int *>(malloc(sizeof *p));, no cast is required when using new. Because new is type-aware, it can also initialize the newly created objects, calling constructors if appropriate. The example above uses this ability to initialize the int created to have the value 3. Another enhancement of new and delete compared to malloc and free is that the C++ standard provides a standard way to change how new and delete allocate memory; in C this is normally achieved using a non-standard technique known as "interpositioning".

Note:
All dynamically allocated memory must be released before the pointer (except smart pointers) pointing to it goes out of scope. So, if the memory is dynamically allocated for a variable within a function, the memory should be released within the function unless a pointer to it is returned or stored by that function.

To do:

Use a simple intrusive non-generic linked list as a more realistic example of use of new/delete.

Talk about considerations behind whether to set a pointer to NULL after deletion; discuss why delete cannot do this. Mention that a pointer's value cannot be used after it has been passed to delete.

The basic new and delete operators are intended to allocate only a single object at a time; they are supplemented by new[] and delete[] for dynamically allocating entire arrays. Uses of new[] and delete[] are even rarer than uses of basic new and delete; usually a std::vector is a more convenient way to manage a dynamically allocated array.

Note that when you dynamically allocate an array of objects, you must write delete[] when freeing it, not plain delete. Compilers cannot usually give an error if you get this wrong; most likely your code will crash when you run it.

Note:
Bear in mind that NEVER try to free a piece of memory allocated by new with the C free function, or free a piece of memory allocated by C malloc function with delete. This may lead to corruption of data structure and unexpected result.

When a call to delete[] runs, it first retrieves information stored by new[] describing how many elements are present in the dynamically allocated array, and then calls the destructor for each element before deallocating the memory. The actual address of the memory block that was allocated may differ from the value returned by new[] to allow room to store the number of elements; this is one reason why accidentally mixing the array form of new[] with the single-element form of delete may lead to crashes.

Note:
For historical interest only: originally when using delete[] it was necessary to specify the number of elements in the array inside the [], such as delete [number_of_elements] pointer_to_first_element;, but experience showed this to be too error prone, and hence new[] was change so that the number of elements allocated is recorded so that it can later be retrieved automatically by a call to delete[]. The C++ syntax now disallows specifying the number of elements explicitly.

The particularly astute reader might be wondering if it would be possible to eliminate the need to remember which of new/new[] and delete/delete[] to use, and make the compiler figure it out instead. The answer is that it would be perfectly possible, but doing so would add overhead to each single-object allocation (as delete would need to be able to work out whether the allocation was for a single object or an array), and a design principle behind C++ has been that you "don't pay for what you don't use", so the trade-off made is that single object allocations remain efficient, but users have to take care when using these low-level facilities.

To do:
Document operator new and operator delete and try to explain the confusion in terminology. It's clear to talk about a "new expression", but the phrase "the new operator" can be ambiguous, and is often mistaken for meaning "operator new".

The code above will lead to resource leakage (or, in some cases, to a crash). It is a common mistake to release a piece of memory array with delete, but not "array delete" (i.e. delete[]). In this situation, the typedef gives the illusion that "a_string" is a pointer which points a piece of memory enough for a "char" variable but not a piece of memory array. By wrongly executing delete, other than delete[], only the memory allocated for the first element of the array is freed, and letting the memory for those 99 "char" elements be leaked. There are only 99 bytes leaked in this case, but when the array is for holding complex classes with a lot of non-static data members, megabytes of memory is leaked. Also, when the same program which contains this bug runs again, another piece of memory will be leaked.

Thus, in the code above

delete a_string;

should be corrected to

delete[] a_string;

or, better still, a string class such as std::string should be used instead of a plain array hidden behind a typedef.

To do:

Document common mistakes made, and symptoms. double-delete, memory corruption. Also note that sometimes a crash when calling new or delete is caused by memory corruption elsewhere in the code, and mention that tools can help with these problems. Give examples of such tools, both Open Source and commercial.

Consider mentioning the possible optimization that new[] does not need to remember the number of elements if they have trivial destructors, which also explains why it's not possible to call a function from user code to find the number of elements that were allocated; it may not be stored anywhere. Or that might be way too much detail for this book. Opinions?