LinuxDevCenter.com
oreilly.comSafari Books Online.Conferences.

advertisement


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

by George Belotsky
06/19/2003

Dealing with Errors Effectively

The previous article dealt with specifying the memory management errors that are common in C++. Now that you are aware of these errors, the next question is how to keep them out of your code. The answer, of course, is software engineering. If you are expecting a moralistic tirade about how software developers are a bunch of young, reckless, ignorant kids who turn out terrible code because they do not follow proper procedures, you are about to be disappointed. While it is possible to create effective procedures for engineering, engineering itself is never about procedures.

Bureaucracy, in and of itself, is not necessarily harmful, as long as it is confined to a supporting role. The problem with any kind of rules and procedures is that they tend to assume a primary importance. Soon, following the rules is the only thing that matters, no matter how horrible the result. For an eloquent description of the consequences, read Richard P. Feynman's outstanding "What Do You Care What Other People Think?" [Fey88].

If engineering is not the act of following a procedure, than what is it? First and foremost, engineering is design, and design is ultimately about dealing with errors. The fundamental need to deal with errors is recognized in every engineering field except software.

Actually, the critics of software development bear a great deal of responsibility for the current state of affairs. While they decry the generally poor quality of computer programs, these critics nevertheless promote the view that software should be flawless. In their view, it is only the impure programmer that ruins the ideal of the perfect program. This sort of archaic idealism makes engineering literally impossible by its failure to recognize that software exists in the real world just like anything else that people build.

Related Reading

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

Unfortunately, it is beyond the scope of this article to discuss the overall merits of various philosophies, the important lessons that things like Godel's Incompleteness Theorem can teach us about computer programs, or even the reason why software errors are inevitable (it has to do with trying to sustain exponential growth with resources that can only increase linearly). Nevertheless, one very important, practical point must be made. You need to accept that software errors will happen (yes, even in your code), then design to minimize their severity and impact just like real engineers do.

Observations on the C++ Memory Management Mechanism

Understanding the C++ memory mechanism will allow you to move beyond identifying specific errors in your code (covered in the previous article) and begin to engineer such errors out of your system. This knowledge will also help you combine various memory handling techniques (presented in the next article) into a coherent design.

Memory Management is About Consistency of Ownership

Since C++ does not provide an automated memory management system, every program is responsible for both allocating and releasing any memory that it needs. This idea is commonly expressed as a relationship of ownership. Each chunk of allocated memory belongs to a clearly identifiable part of the program (typically an object) which is responsible for freeing that memory when appropriate. The concept of memory ownership is used to create a comprehensible, ordered design for what would otherwise be a chaotic, error-prone program.

Memory ownership focuses on managing the deallocation of memory, preventing leaks and especially dangling references. Memory allocated in one part of the program might subsequently become the responsibility of another part. This is called transfer of ownership.

Ownership is a very useful idea in thinking about your design. When you allocate memory, you must decide who owns it (i.e., which part of your program will ultimately be responsible for freeing that memory). Good judgment here will go a long way toward preventing memory leaks and dangling references.

By far the most important aspect of memory ownership is that it must be consistent. If you have an object that is handling some memory, every method of that object's class must be based on the same assumptions about the ownership of that memory. If one method is written as if the object itself owns the memory while another assumes that the memory is owned by something else, disaster will follow.

It actually turns out that the classic dangling reference scenario illustrated in the previous article is really a case of inconsistent memory ownership. In the example, the destructor frees some memory that is used by SimpleString, a clear indication that SimpleString objects own the memory. Unfortunately, no copy constructor or assignment operator consistent with this view of ownership is provided. Instead, the C++ compiler is allowed to generate the default versions of these methods. The defaults just copy the pointer member of SimpleString as if some other part of the code were responsible for the memory. The SimpleString destructor, of course, assumes otherwise; a dangling reference is the result.

The next article in this series covers a number of techniques for memory management. As you read read about these methods, however, keep in mind that successful memory management with C++ demands consistency of ownership for everything that you allocate.

Memory Allocation is a Side Effect

Consider this innocent-looking piece of code.

Example 1 — a surprising result: the Code

//---
try {
  Surprise surprise;

}
//Catch all standard exceptions. 
catch (exception& e) {
  cout << "caught an exception of type " 
       << typeid(e).name() << endl; 
}
//---

Here is the output that this code produces on the author's system.

Example 2 — a surprising result: the output

caught an exception of type St9bad_alloc

Not so innocent, after all, but why does it throw a bad_alloc exception? Such an exception can only result when an attempted memory allocation fails, but there is no call to new in the code sample given.

The answer, of course, lies in the class Surprise.

Example 3 — the Surprise class

//---
class Surprise {

public:
  Surprise();
  ~Surprise();

private:
  char* huge_cstr_p_;

  //See the Training Wheels Class in article three of this
  //series for an explanation of these declarations.
  Surprise(const Surprise&);
  Surprise& operator=(const Surprise&);
};

Surprise::Surprise() : huge_cstr_p_(0) {
  //Try to allocate an absolutely gigantic buffer.
  huge_cstr_p_ = new char[2000000000];
}

Surprise::~Surprise() {
  delete [] huge_cstr_p_;
}
//---

Surprise tries to allocate a very large amount of memory, which results in an exception when new fails. Clearly, memory allocation is a side effect.

In general, programmers are taught to avoid side effects for the very reason just illustrated: side effects surprise the users of your code. While dynamic memory allocation cannot always be avoided, it is important to be aware that hidden allocation inside classes is a side effect. When you allocate memory, the user suffers a loss of control. In this example, only a local object of class Surprise was created. The user of your class might specifically want to avoid allocating from the heap at this point in the program, but you force her to do so.

Before allocating memory, consider alternatives that might be available. The following list provides several suggestions.

  • Allow the user to supply her own buffer (e.g., as a parameter to the constructor).

  • Use a member object instead of a pointer to a dynamically allocated object.

  • Strictly local objects that are used only for the duration of a method call should be declared as automatic variables.

  • Use an allocator object to get the memory, and allow the user to specify his own allocator.

Allocators are commonly used in the C++ standard library as well as other libraries. An architecture that supports allocators allows a great deal of flexibility in memory management. The strategy used to allocate memory can be changed by replacing allocators, leaving the rest of the program intact. In particular, if the users of your class are allowed to supply an allocator of their choice (e.g., through one of your class's constructors) they can implement their own memory management strategy to work together with your code.

Pages: 1, 2

Next Pagearrow




Linux Online Certification

Linux/Unix System Administration Certificate Series
Linux/Unix System Administration Certificate Series — This course series targets both beginning and intermediate Linux/Unix users who want to acquire advanced system administration skills, and to back those skills up with a Certificate from the University of Illinois Office of Continuing Education.

Enroll today!


Linux Resources
  • Linux Online
  • The Linux FAQ
  • linux.java.net
  • Linux Kernel Archives
  • Kernel Traffic
  • DistroWatch.com


  • Sponsored by: