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


C++ Memory Management: From Fear to Triumph, Part 3

by George Belotsky
08/07/2003

Techniques for C++ Memory Management.

The first article in this series covered common C++ memory errors, while the second described the general nature of the C++ memory management mechanism. Now it's time to present a list of simple, powerful techniques that you can use to deal with memory in your C++ programs. It is best not to read this article in isolation; the previous articles will help you use the techniques presented here much more effectively.

An Ownership-Consistent SimpleString -- the Classic Example Continued

The first article of this series demonstrated a classic dangling reference by defining a class called SimpleString. Subsequent discussion in the second article showed that the problem in SimpleString is actually caused by divergent assumptions about memory ownership among the methods of that class. (The concept of memory ownership is critical: you should read about it now if you have not done so already.) In the original SimpleString, the destructor is written as if SimpleString objects own the memory used for their character buffers. The default copy constructor and assignment operator, however, act as though something else were responsible for that memory.

One way to make all of SimpleString behave consistently is to write our own copy constructor and assignment operator for the class, instead of relying on the default versions of these methods supplied by the C++ compiler. The following two examples illustrate this approach to fixing SimpleString.

Secure Programming Cookbook for C and C++

Related Reading

Secure Programming Cookbook for C and C++
Recipes for Cryptography, Authentication, Input Validation & More
By John Viega, Matt Messier

Example 1. simplestring.h

//*** SIMPLESTRING DECLARATION *** 

#ifndef EXAMPLE_SIMPLE_STRING
#define EXAMPLE_SIMPLE_STRING

class SimpleString {
  
public:

  explicit SimpleString(char* data = "");  //Use 'explicit' keyword to disable
                                           //automatic type conversions --
                                           //generally a good idea.

  //Copy constructor and assignment operator.
  SimpleString(const SimpleString& original);  
  SimpleString& operator=(const SimpleString& right_hand_side);

  virtual ~SimpleString();   //Virtual destructor, in case someone inherits
                             //from this class.

  virtual const char* to_cstr() const;  //Get a read-only C string.

  //Many other methods are needed to create a complete string class.
  //This example implements only a tiny subset of these, in order
  //to keep the discussion focused.

  //N.B. no 'inline' methods -- add inlining later, if needed for
  //optimization.

private:
  char* data_p_;   //Distinguish private class members: a trailing underscore
                   //in the name is one common method.
  
};

#endif

//*** END: SIMPLESTRING DECLARATION ***

Example 2. simplestring.cpp

//*** SIMPLESTRING IMPLEMENTATION *** 

#include <cstring>

#include "simplestring.h"

using namespace std;

//Constructor 
SimpleString::SimpleString(char* data_p) :
  data_p_(new char[strlen(data_p)+1]) {
  strcpy(data_p_,data_p);
}

//Copy constructor.
SimpleString::SimpleString(const SimpleString& original) :
  data_p_(new char[strlen(original.data_p_)+1]) {
  strcpy(data_p_,original.data_p_);
}

//Assignment operator.
SimpleString& 
SimpleString::operator=(const SimpleString& right_hand_side) {
  
  //It is possible for the caller to request assignment to self 
  //(i.e. "a = a;").  Do nothing in this case, or a serious
  //error will result.
  if (this != &right_hand_side) {

    //Allocate a new buffer first.  If this fails (i.e. throws
    //an exception), everything is still consistent. 
    char* data_p = new char[strlen(right_hand_side.data_p_)+1];

    //Now, delete the old buffer, and start using the new one.
    delete [] data_p_;  //(1)
    data_p_ = data_p;   //(2)
    
    //Copy the data over from the right hand side.  We checked
    //before that this is not self assignment, so we are safe.
    //Otherwise, we would have already destroyed this data
    //in statements (1) and (2)!
    strcpy(data_p_,right_hand_side.data_p_);
  }

  //This allows assignments to be chained (i.e. "a = b = c = d;").
  return *this;
}

//Destructor
SimpleString::~SimpleString() {
  //N.B. Use of 'delete []' corresponds to previous use of 'new []'.
  //     Using just 'delete' here would be a disaster.
  delete [] data_p_;
}

//Returns a read-only C string representation.
const char* SimpleString::to_cstr() const {
  return data_p_;
}

//*** END: SIMPLESTRING IMPLEMENTATION ***

As you can see, providing your own copy constructor and assignment operator is quite a bit of work. Fortunately, there are alternatives that will often make the defaults work as intended. The C++ standard library offers solutions for many such situations, for example. Smart pointers are another possibility and so is the Training Wheels class. Still, there will always be cases where a good design requires custom constructors and assignment operators.

The Training Wheels Class

Here is a useful example of software engineering in action. Ask yourself "What if someone tried to copy an object of my class? What if they attempted to assign one object of my class to another?" As shown in the first article, the default copy constructor and assignment operator that C++ supplies are often dangerous. You may not have the opportunity to write your own versions of these methods right away. You may even see no need for anyone to copy or assign objects of your class and so wish to disallow these operations altogether. Clearly, safer default behavior would be very useful. This is the purpose of the Training Wheels class.

Example 3. The Training Wheels class

//---
//Pattern your classes on this class, at least
//during initial development.  It is safer than
//the defaults that the C++ compiler generates.
class TWC {

public:
  //The default constructor, just so that the examples
  //work.  You'll define your own constructors, which
  //may or may not include the default constructor.
  TWC();  
  
  //You may want to include a virtual destructor,
  //but if derived classes will will define
  //actual copy constructors and assignment
  //operators, there is no way to copy the base
  //class, so be careful.

private:
  //The copy constructor and assignment operator 
  //are disabled.  Clients of this class that attempt 
  //to use them will cause errors at compile time.
  TWC(const TWC&);
  TWC operator=(const TWC&);

};

//The do-nothing default constructor.
TWC::TWC() {}
//---

The key feature of the Training Wheels class is that the copy constructor and assignment operator are declared private. Thus, when someone tries to copy an object of your class or assign one object of your class to another, the compiler generates an error message. The copy constructor and assignment operator being private are not accessible. For example, here is what happens when a program tries to assign one instance of the Training Wheels class to another (the exact wording of the error messages is compiler dependent).

Example 4. The Training Wheels class: attempted assignment

$ g++3 twct.cpp
twc.ex: In function `int main()':
twc.ex:24: `TWC TWC::operator=(const TWC&)' is private
twct.cpp:11: within this context

Related Reading

C++ In a Nutshell
A Desktop Quick Reference
By Ray Lischner

Forgetting to define the copy constructor and the assignment operator is a common error. The Training Wheels class does not prevent you from making it. Users of your class are unlikely to be happy when their code does not compile due to your mistakes. A compiler error, however, is a far more preferable outcome than the disaster of a dangling reference. The Training Wheels class is not a recipe (even in a very local, limited sense) for error-free software; that would be impossible. Instead, the class is an honest recognition of the possibility of errors together with a conscious, calculated attempt to contain the damage. To use the Training Wheels class as the starting point for your own classes constitutes a concrete, practical act of software engineering.

The true engineer knows that the real world is full of uncertainty and error, that her calculations no matter how meticulously done can never capture this world in all of its complexity. The true engineer asks, for example, "what would happen if my estimate of the maximum wind loading on this building is off by a fifty percent, or by a factor of two, or a factor of five?". She accepts the reality of an uncertain world, indeed of her own human fallibility in that world, and it is only through this acceptance that she may achieve a good design. Of course, it is always possible to go too far with such reasoning; the result is commonly referred to as "over-built" or "over-engineered". Nevertheless, this kind of thinking is central to a practical design. We refer to it as adding a safety margin.

In considering the Training Wheels class, we acknowledge the fact that software engineers, too, live in a world of uncertainty and error. The very human act of creating software makes it so. Thus, the true software engineer asks, for example, "what would happen if someone tried to copy an object of my class, or to assign to an object of my class," accepting all the while that mistakes are possible. Using the Training Wheels class instead of the C++ default is an engineer's way of dealing with the very real possibility of these mistakes. It is an example of a safety margin in software.

Objects that Act Like Pointers

In C++ you can create classes that are almost an extension of the language. We have already seen an example of this with the assignment operator. The syntax of assigning objects to one another is exactly the same as that for integers, doubles, or any other built-in C++ type. When you define your own assignment operator, however, you are effectively determining the semantics of assignment for objects of your class. The syntax (roughly, how something is expressed) remains the same and is fixed by C++. The semantics (roughly, what something means) are up to you.

The ability to determine the meaning of various operations as they apply to your classes can be brought to bear on a very problematic feature that C++ has inherited from C: the pointer. As you have already seen in the first article, manipulating memory via pointers can be very dangerous because memory leaks and dangling references occur quite readily. While C++ is far less dependent on pointers than C, they are still a highly useful tool in many situations. It is not desirable to give up pointers altogether: what we really want is a safe pointer.

How is it possible to achieve a safe pointer without rewriting the language? In C++, we simply create a class that behaves like a pointer. We define the pointer dereference operation for our class, and make everything else roughly consistent with pointer behavior. Most of the code using our pointer-like object looks exactly the same as if we were using ordinary pointers. Our "pointers", however, can be made to do a great deal more, such as preventing dangling references. Compared to ordinary, dumb pointers, these pointer-like objects seem very intelligent. Hence, their common name: smart pointers.

Probably the first smart pointer that you will encounter is the standard C++ library auto_ptr. Here are two sample pieces of code. Both are correct, but the first uses ordinary pointers, while the second takes advantage of the standard auto_ptr.

Example 5. Handling memory via an ordinary pointer

//---
Base* obj_p = 0;
try {
  obj_p = new Derived;

  throws_exception();

}
//Must catch everything, so that "obj_p"
//does not leak.
catch(...) {
  delete obj_p; //Cleanup.
  throw;        //Rethrow.
}
//---

Example 6. Handling memory via the standard auto_ptr

//---
//Can assign a derived object to a base 
//pointer just like with ordinary pointers.
auto_ptr<Base> obj_sp (new Derived);

//The auto_ptr "obj_sp" will free the 
//object that it points to (if any).
throws_exception();
//---

It is clear that the second example is much less prone to errors. The output is the same in both cases (assuming something eventually catches the exception without rethrowing it).

Example 7. Handling memory via pointers: output

Deleting Derived Data, Deleting Base Data

The auto_ptr behaves like an ordinary pointer in many respects. When an auto_ptr goes out of scope, however, it automatically deletes the memory that it is holding — something that the ordinary pointer does not do.

In addition to memory leaks, the auto_ptr also prevents dangling references. When you assign one auto_ptr to another, the auto_ptr on the right hand side actually becomes the equivalent of a NULL pointer! The target of the assignment (on the left hand side) now points to the memory that the right hand side auto_ptr pointed to before. This is a powerful example of how the semantics of an operation (in this case assignment) can be defined in C++.

Recall our discussion in the second article about consistency of ownership. The auto_ptr maintains such consistency. First, it owns the memory that it points to. Second, the semantics of assignment for auto_ptrs is transfer of ownership. The target of the assignment becomes the new owner of the memory, while the source turns into a NULL pointer, which owns nothing. The two diagrams below contrast the assignment semantics of ordinary pointers with those of the auto_ptr.

Block Diagram
Figure 1. Assignment semantics of ordinary pointers

Block Diagram
Figure 2. Assignment semantics of the auto_ptr

The auto_ptr is a very simple smart pointer, but it has many uses (see the article Using auto_ptr Effectively, for example). Its copy semantics, however, make it unsuitable for some operations. Most notably, the auto_ptr cannot be used in STL containers (the STL is part of the C++ standard library, and is discussed next in this article). It is not necessary, however, to restrict yourself to just one kind of smart pointer. Boost.org, for example, provides free implementations of several smart pointers. Their shared_ptr is based on reference counting (an elegant, efficient memory management technique that can provide similar benefits to garbage collection) and is safe to use in containers.

The smart pointer is a powerful idea for writing better C++ programs. The standard auto_ptr, the offerings at Boost.org, and many other implementations provide you with lots of choices for various tasks. Using them as examples, you might even write smart pointer classes yourself with appropriately fine-tuned semantics for your application.

Be Aware of C++ Alternatives to Traditional C Methods

Because C++ is largely compatible with C, it is much easier to port C code into a C++ environment. It does not mean, however, that you should program in C++ as you would in C. In fact, the C++ standard library provides safe, simple solutions to many problems that are very troublesome in C.

For example, arrays and strings are two problematic issues that the C++ standard library solves very well. Here are two programs to illustrate this point. The first is written in the traditional C style, while the other one uses the new features of the C++ standard library.

Example 8. Array of strings, C style

//*** C STYLE ARRAY OF STRINGS *** 

#include <stdio.h>

void print_strings(char** strings, unsigned num) {
  
  for (int i = 0; i < num; ++i) {
    printf("%s\n",strings[i]);
  }
}

int main() {

  char* strings[3] = {"One", "Two", "Three"};

  print_strings(strings, 3);
}

//*** END: C STYLE ARRAY OF STRINGS ***

Example 9. Array of strings, C++ style

//*** C++ STYLE ARRAY OF STRINGS *** 

#include <iostream>
#include <string>
#include <vector>
using namespace std;

void print_strings(const vector<string>& strings) {
  
  for (int i=0; i < strings.size(); ++i) {
    cout << strings[i] << endl;
  } 
}

int main() {

  vector<string> strings(3);
  strings[0] = "One"; strings[1] = "Two"; strings[2] = "Three";
  
  print_strings(strings);
}

//*** END: C++ STYLE ARRAY OF STRINGS ***

Both examples produce the same output.

Example 10. Array of strings: output

One
Two
Three

The second example uses the string class from the standard C++ library. It also uses the vector template class, which implements a resizable array. The vector template is part of the Standard Template Library (STL), a powerful, elegant, and highly extensible set of algorithms and data structures. (STL data structures such as vector are generally referred to as containers because they are designed to hold other objects). The STL is included in the C++ standard library. Performance of the first and second example is similar, except in the initial step, where filling the vector with string objects is significantly slower. In many real-world situations, the initialization penalty for the C++ style solution would be much smaller.

At first glance, the C++ example is more complex, but not overly so. Its increased safety, however, is already apparent; vector knows its own size. Thus, misstating the length of the array when calling print_string — a disastrous error in the C style version — is entirely avoided.

There's a lot more here that meets the eye, however. The true difference between the C and C++ versions is in responsibility for memory management. In the C++ example, vector and string handle their own memory. Each string, for instance, will automatically clean up its character buffer when the vector is destroyed, even if all of the objects were dynamically allocated. If the strings in the C example were dynamically allocated, however, it would be necessary to provide extra code to delete them. As a small illustration of the capabilities of the C++ programming style, here is a slightly modified version of the array of strings example.

Example 11. Array of strings, C++ style (version 2)

//*** C++ STYLE ARRAY OF STRINGS (VERSION 2) *** 

#include <iostream>
#include <string>
#include <vector>
using namespace std;

void print_strings(const vector<string>& strings) {
  
  for (int i=0; i < strings.size(); ++i) {
    cout << strings[i] << endl;
  } 
}

int main() {
  vector<string> strings(3);
  strings[0] = "One"; strings[1] = "Two"; strings[2] = "Three";
  
  print_strings(strings);

  strings[0] += " and a";
  strings[1] += " and a";

  strings.resize(4);
  strings[3] = "and One, Two, Three!";

  cout << endl << "Encore!" << endl << endl;
  print_strings(strings);
}

//*** END: C++ STYLE ARRAY OF STRINGS (VERSION 2) ***

Here is the output.

Example 12. Array of strings (version 2): output

One
Two
Three

Encore!

One and a
Two and a
Three
and One, Two, Three!

The examples shown here should give you an understanding of how C++ features can be applied to traditional C problems. Many variations are possible. For example, a Boost.org shared_ptr (covered previously) could be used with vector. This would keep the automated resource management, while allowing the vector to effectively contain objects belonging to an inheritance hierarchy (note that you should never use the standard auto_ptr inside vector or other STL containers).

Doing things the C way still has its place. Working with legacy code, developing kernel-level software, or creating an embedded system often requires the older C style. Many decades and many platforms later, C is nowhere near obsolete. Nevertheless, the C++ alternatives are often the better choice, particularly for user-level code. In general C++ development, it is a good rule of thumb to favor the C++ solutions over traditional C approaches, resorting to the latter only when it is clearly necessary.

Learn to Love C++ References

The C++ reference seems at first such an ungainly thing. Why would anyone want an alias for an existing object? Isn't it merely confusing? This skepticism of references is usually due to the way they are presented. The real place where references make a profound difference is in parameters to functions and catch clauses. Under these circumstances, references are overwhelmingly desirable, as shown in the following list.

Even when preventing modifications to objects passed to a function is an issue, a const reference can often be used instead of passing by value. A well-designed class will declare all methods that do not actually modify the state of the object as const, signifying that calling these methods on a const object is allowed. Thus, read-only operations will work on const references to objects of such classes, making pass-by-value only a rare necessity. (Remember not to return a const parameter by const reference, however.)

Now it's clear that references are a very important tool for better C++ programs. They are safe, efficient, and don't slice objects. References should be your first choice for function and method parameters. For exceptions, the rule is even more strict. Not catching an exception by reference is almost always a mistake.

Don't Sacrifice Program Correctness to Performance

C++ makes high performance possible. Indeed, this is frequently a major reason for choosing C++. Unfortunately, it is not always feasible to maximize efficiency while having the program function correctly. When that happens, program correctness must always win out over performance. In practical terms, it should not be difficult to take this approach. Only about 20 percent of a program is typically the performance bottleneck [[Mey96]], and it is important to rely on profiling tools to find that 20 percent. "Programmer's intuition" is most often wrong regarding these bottlenecks.

In general, choosing an efficient design with good algorithms, along with a straightforward implementation (usually in C++ style, as shown previously) will yield excellent performance in a C++ program. If further optimization is necessary, it should be undertaken with the aid of analytical tools.

Regarding memory management, there is a critical area where incorrect optimization leads to errors. C++ programmers, having learned the lesson about the benefits of references, are very reluctant to return objects by value from their functions and methods. The efficiency concerns are understandable: returning an object by value generates temporary objects, with their attendant construction and destruction costs. Unfortunately, references, so useful as parameters, are rarely desirable in return values. The many errors that are possible when returning objects by reference or pointer have been previously covered in the first article.

If you return dynamically allocated objects from your function, you should generally do so via a pointer. The auto_ptr is also very useful in such cases. In many situations, however, it is impossible to get around returning an object by value. Fortunately, the compiler frequently comes to your rescue [[Mey96]]. The temporary objects generated for a return-by-value can be automatically avoided by the compiler. The following code shows how you can help this process, by giving the compiler a hint. Of course, the optimization is compiler dependent, and some compilers will even do the right thing even without the hint.

Example 13. Return value optimization

//---
SimpleString return_by_value() {

  //Constructing the return value like this (as part
  //of the "return" statement) helps the compiler
  //eliminate the temporary objects associated with
  //return-by-value.
  return SimpleString("Return Value Optimization");
}
//---

While avoiding return-by-value is a worthwhile goal, be prepared for the fact that it may not be possible. In those situations, give the compiler a hint as shown in the example, and accept whatever performance hit remains. It is indeed a poor bargain to allow a memory leak or a dangling reference in order to get a boost in performance.

Memory Allocation Considered Harmful

The goto. The pointer cast. Memory allocation? Unfortunately, not all harmful things in this world can be avoided. Memory allocation is one of them. After reading this series of articles, you probably would not be surprised to learn that the majority of both C++ and C errors are related to memory. Yes, memory allocation truly is harmful.

The prescription for allocating memory is actually similar to the one for goto. You can, in fact, avoid memory allocation in the majority of your code by encapsulating memory management in its own subsystem. Operations with memory are like toxic chemicals: extremely useful if confined to a small area and put to work, but terribly destructive if allowed to leak out. This series of articles presented several techniques, such as smart pointers and allocators, that can restrict memory operations to a well-defined part of the code. Keep your memory operations bottled up, where you can use them to your benefit in relative safety, and wonderful things should happen to your program.

Memory allocation: it's harmful but good!

Series Conclusion: Every C++ Program Needs a Memory Management Design

If you remember only one thing from this article series, it is that every C++ program needs a memory management design. After all, the designers of memory-managed languages such as Java and Python have put a great deal of thought into their automated memory management schemes. In C++, this task is up to you.

If you will be developing a subsystem for a specific application rather than a general-purpose language, your approach to memory management can be much simpler and much more focused. Nevertheless, you will need an approach. Ad-hoc calls to new all over your program will just leave you chasing memory leaks and dangling references, which is a thankless task that quickly becomes impossible as the program grows. A sound memory management design, on the other hand, will allow you to fine-tune your code safely, in ways not possible with a memory-managed language. This is the key to effective use of C++.

Further Reading

This information appears in the previous two articles. It is included again for your convenience. A number of very useful resources are available regarding C++. Notes on these resources are provided here (the Bibliography itself follows).

First, you need a book with broad coverage, which can serve as an introduction, a reference, and for review. Ira Pohl's C++ by Dissection [[Poh02]] (for which the author of this article was a reviewer) is an example of such a book. It features a particularly gentle ramp-up into working with the language.

In addition to a book with broad coverage, you will need books that focus specifically on the most difficult aspects of the language, and present techniques to deal with them. Three titles that you should find very valuable are Effective C++ [[Mey98]], More Effective C++ [[Mey96]] (both by Scott Meyers) and C++ FAQs [[Cli95]] (by Marshall P. Cline and Greg A. Lomow). There is also an online version of the last title.

The key to reading all three books is not to panic. They contain a great deal of difficult technical details, and are broken up into a large number of very specific topics. Unless you are merely reviewing material with which you are already familiar, reading any of these books from cover to cover is unlikely to be useful.

A good strategy is to allocate a little time (even as short as 15 minutes) each day to work with any one of the Meyers' books, or with C++ FAQs. Begin your session by looking over the entire table of contents, which in all three books has a very detailed listing of all the items covered. Don't ignore this important step; it will take you progressively less time as you become familiar with each particular book.

Next, try to read the items that are most relevant to the current problem that you are trying to solve, ones where you feel that you are weak, or even those that seem most interesting to you. An item that looks completely unfamiliar is also a good candidate it is likely an important aspect of C++ that you are not yet aware of.

Finally, when you want insights into bureaucracy, tips on what to do with your icewater during NASA meetings (answer: dip booster rocket O-ring material into it), or just a good laugh when you are frustrated with C++, try Richard P. Feynman's "What Do You Care What Other People Think?" [[Fey88]]. The second article in this series describes why Feynman's book is so important.

Bibliography

This bibliography also appears in the previous two articles of this series. Notes on the bibliography are given in Further Reading.

[Cli95] Marshall P Cline and Greg A Lomow. C++ FAQs. Frequently Asked Questions. Addison-Wesley Publishing Co., Inc.. Copyright 1995. 0-201-58958-3.

[Fey88] Richard Feynman and Ralph Leighton. "What Do You Care What Other People Think?". Further Adventures of a Curious Character. W.W. Norton & Company, Inc.. Copyright 1998 Gweneth Feynman and Ralph Leighton. 0-393-02659-0.

[Mey96] Scott Meyers. More Effective C++. 35 New Ways to Improve Your Programs and Designs. Addison-Wesley Longman, Inc.. Copyright 1996. 020163371X.

[Mey98] Scott Meyers. Effective C++. 50 Specific Ways to Improve Your Programs and Designs. Second. Addison-Wesley. Copyright 1998. 0-201-92488-9.

[Poh02] Ira Pohl. C++ by Dissection. The Essentials of C++ Programming. Addison-Wesley. Copyright 2002. 0-201-74396-5.

George Belotsky is a software architect who has done extensive work on high-performance internet servers, as well as hard real-time and embedded systems.


Return to the Linux DevCenter.

Copyright © 2009 O'Reilly Media, Inc.