Everyone knows that memory management is a difficult and dangerous chore in C++. This series of three articles will show you that the conventional wisdom is not true. When approached correctly, C++’s seemingly archaic memory-management scheme actually provides an opportunity to create spectacular programs – programs that would not be possible with more modern languages that handle memory automatically.
This article, part one in a series, discusses C++ in the context of several other popular languages. It also describes the kinds of memory errors that can occur in C++ programs. The most common specific errors are then presented in a set of tables, for easy reference when developing your own code.
Subsequent articles in the series will demonstrate a different way of thinking about memory management ? not as a dreaded danger to be faced with stoic resolve, but as a powerful and subtle tool for improving your work. In many situations, the C++ approach to memory resources is not cause to avoid the language, but actually the reason why it should be used!
Choosing the Right Language: C++ and Some Alternatives
Not so long ago, C and C++ were about the only practical choices for serious software development on microcomputers. This is no longer the case. Java, Python, and Perl are three established, mainstream alternatives that can be used for many applications today. These new languages run on multiple platforms, feature extremely rich libraries for everything from encryption to graphics, and are supported by large, enthusiastic user communities. They are freely available (although Java’s source code is not open like the other two). All three are widely used in business, government, and research.
In contrast to C and C++, the newer languages (Java, Python, and Perl) have automated memory management. This ranges from Python’s simple reference-counted model to Java’s sophisticated garbage collector. To the programmer, however, the final result is nearly always the same: no more worrying about memory errors. Why, then, even bother considering C or C++? Why not just choose one of the memory-managed languages and live happily ever after?
Sometimes, a Memory-Managed Language is Really the Best
In many applications, a memory-managed language is indeed the right choice. Scripts to automate a small to medium-sized web site, for example, are best written in Perl or Python. To analyze your server’s logfile, Perl is probably the best choice. A single-user application with an excellent GUI can be built in Java. Complex object-oriented programs are quickly and easily written in Python.
Even older systems written in C or C++ can benefit from the addition of new code in one of the memory-managed languages; Java, Python, and Perl all provide features that make such integration possible. Do C and C++, which require low-level operations to handle memory, have any significant role left to play in modern software development? The answer is yes, as the following discussion will show.
When a Low-Level Tool is Better
Languages that give you precise control over system resources remain highly relevant to this day – and not only for embedded systems or kernel-level code. If your project is a user-space server or must perform lots of computation (e.g., a graphics package or a game), you will save yourself a great deal of trouble by avoiding the memory-managed languages, at least for the core of your system.
As described previously, many situations are handled by the automated, general-purpose allocation strategies of the memory-managed languages. In many other cases, however, resource management becomes critical. Making efficient use of a server’s RAM, for example, is a daunting task with a memory-managed language. Your control is minimal, so precisely matching data structures to algorithms is a highly uncertain process.
It is possible to learn how to use a language such as C++ correctly. On the other hand, trying to get Java’s garbage collector (just to pick one example) to take a specific action at a certain time is an impossible task. Sometimes it will work, then break with no explanation. The behavior will depend on which JVM you are using, so your code will not be portable. Even on the same platform, JVM upgrades will unleash mystifying, subtle errors. This is precisely the kind of nightmare that is typically associated with memory allocation errors in C++!
Because memory-managed languages seem so automatic at first glance, many programmers assume that their greatest worries are over once they choose such a language. Hopefully, their luck will hold up, and they can live a happy life. For the multitude that will not be so fortunate, there is a hard and painful lesson waiting here: errors in memory management almost always lead to disaster. A memory-managed language does not change that; it gives you a generic (albeit well-designed) strategy to deal with the problem.
If, however, the generic strategy is wrong for your project, then you will have made a memory management error, and you will pay for this error just as surely as if you had coded it yourself.
C++ or C?
User-space programs are where C++ really shines. While kernel development, embedded software, and hard real-time systems can also benefit from the use of C++, the simple, predictable C is often the better choice for such applications. In contrast, C++’s richness is often preferable to C’s simplicity in user space.
Just like C, C++ will give you precise control over system resources. Also, for a very slight performance penalty over C (a few percentage points at most for a well-written program) C++ offers many more tools for the capable designer. A good architecture for a complex project is much more easily expressed in C++.
C++ is highly suitable for large projects done by small, competent teams. A group of several developers willing to treat C++ with respect will get exceptional results with the language. Using C++ in larger teams can be risky, however. In such situations, it is probably best to build the core of the system in C++, and then augment it with a memory-managed language. Such an arrangement is much more forgiving of programmer error than having several hundred people code only in C++.
If you must use C++ exclusively on a project with many developers, you’ll need to make a small group responsible for all memory allocation. Everyone else would then rely on the facilities developed by that group (all code should actually be scanned, making sure that calls to new
and delete
are restricted to the designated part of the program). Almost all classes developed by a large team should work correctly with C++ default copy constructors and assignment operators (the technical details will be covered in articles two and three of this series). You will also need a system to track the classes that require custom implementations of these methods, because such classes can easily lead to very dangerous and subtle memory errors.
While C++’s demands for low-level coding may look clumsy at first glance, elegant C++ programs (such as the widely used Standard Template Library) are amongst the most beautiful of all software. Some truly amazing things are indeed possible with the language; hopefully, this series of articles will help you achieve some of these things yourself.
What Was Not Covered
The modern programmer has a truly breathtaking choice of programming languages. It is beyond the scope of this article; whose main focus is C++; to discuss them all. In the language overview provided here, several excellent mainstream languages are discussed. Of course, there is a lot more out there: Smalltalk, Fortran, Forth, COBOL, Ruby, Lua, Scheme, Guile, etc. One of these other languages might be the right one for you or your project. So keep exploring, and don’t forget that older languages such as C++ are not automatically obsolete even when a great new invention (e.g., Python) comes along.
Having thought about C++ and its alternatives, it is now time to take a specific look at C++ memory management, and see what elegant programs you yourself can build with its powerful, subtle tools.
By George Belotsky Initially published on Linux DevCenter (http://www.linuxdevcenter.com/)
Understanding C++ Memory Errors
The first step to good C++ memory management is understanding the major errors that are common in this area. An overview is given here; there are also a number of excellent books that cover memory errors in C++.
Even when presented in a condensed form, there are enough subtleties in C++ memory management to result in quite a number of warning and caveats. The key is not to get discouraged at the start. After describing the potential dangers, subsequent articles in this series will proceed to show you how to overcome them with several simple, straightforward techniques. You will also see how the power of good software design can go beyond merely coping with problems, and turn C++ memory management into a powerful tool.
As an aside, note that utilities are available to help detect memory-related errors. This series of articles, however, focuses on what you can do when you design and code your programs. Together with a good design and careful coding, the tools can certainly add value. The first strategy, however, must always be to architect memory errors out of your system. This is the only way to make programs that work great, rather than ones that just barely pass a test. In the end, the best tool is still between your ears.
A Memory Error Taxonomy
Memory errors come in two basic types: the dangling reference and the memory leak. The former happens when memory is freed up, but some other code still maintains a reference or pointer to the released area as though it were still allocated. The latter happens when a design calls for allocation and deallocation of memory, but the deallocation step is left out (maybe only in some places) by mistake. The program keeps allocating memory, but does not free all of it; the amount of total available memory in the system keeps going down until something critical breaks because it cannot get the memory it needs.
Strictly speaking, a dangling reference is part of a larger set of errors sometimes known as the wild pointer. A wild pointer can also be generated by forgetting to initialize a local pointer variable (i.e., it then points at some random location), or by setting the pointer to an incorrect value. Fortunately, these errors are typically eliminated by very basic good programming practices. Dangling references are much more complex and subtle.
Out of the two major types of non-trivial memory errors, dangling references are by far the deadliest ? they are hardest to debug, and the least susceptible to detection by automated tools. While you should not forget about the danger of memory leaks, dangling references should be your first concern.
From Memory Leak To Dangling Reference?
Here is a classic case of how a memory leak is introduced into a C++ class implementation. Unfortunately, the fix causes something even worse: a dangling reference.
First, the original code, which causes the leak.
Example 1. A Common Memory Leak
//*** A TYPICAL MEMORY LEAK *** #include <iostream> //N.B. no ".h": new-style include. #include <cstring> using namespace std; //Everything in 'std' is accessed directly. //A simple string class. class SimpleString { public: explicit SimpleString(char* data = ""); //Use 'explicit' keyword to disable //automatic type conversions -- //generally a good idea. 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. }; //Constructor. SimpleString::SimpleString(char* data_p) : data_p_(new char[strlen(data_p)+1]) { strcpy(data_p_,data_p); } //Destructor. SimpleString::~SimpleString() { //OOPS, forgot to delete "data_p". } //Returns a read-only C string representation. const char* SimpleString::to_cstr() const { return data_p_; } int main() { //Create a local SimpleString. SimpleString name("O'Reilly Onlamp"); //Print it out. cout << name.to_cstr() << endl; } //*** END: A TYPICAL MEMORY LEAK ***
As you can see, the memory allocated in SimpleString’s constructor was not released in the destructor. This is a common mistake. When a SimpleString object is destroyed, the the memory pointed to by data_p_ is simply lost. It seems that a simple change (deleting the memory in the destructor) will fix this problem. It does, but the dreaded dangling reference is now introduced.
Common Dangling Reference
Example 2. A Common Dangling Reference
//*** A TYPICAL DANGLING REFRENCE *** #include <IOSTREAM> //N.B. no ".h": new-style include. #include <CSTRING> using namespace std; //Everything in 'std' is accessed directly. //A simple string class. class SimpleString { public: explicit SimpleString(char* data = ""); //Use 'explicit' keyword to disable //automatic type conversions -- //generally a good idea. 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 }; //Constructor SimpleString::SimpleString(char* data_p) : data_p_(new char[strlen(data_p)+1]) { strcpy(data_p_,data_p); } //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_; } int main() { //Create a local SimpleString. SimpleString name("O'Reilly Onlamp"); //Print it out. cout << name.to_cstr() << endl; //Dynamically create another SimpleString; make it a copy of the local one. SimpleString* name_copy_p = new SimpleString(name); //Print out the copy. cout << name_copy_p->to_cstr() << endl; //Print out the original again. cout << name.to_cstr() << endl; //Delete the copy; set the pointer to null just in case it's used again. delete name_copy_p; name_copy_p = 0; //This looks fine... but the results are highly system-dependent. cout << name.to_cstr() << endl; } //*** END: A TYPICAL DANGLING REFERENCE ***
This program looks innocent at first glance, but look at the output it
produces on the author’s system.
Example 3. One Possible Result of a Dangling Reference
O'Reilly Onlamp O'Reilly Onlamp O'Reilly Onlamp 3"@3"@ Onlamp Segmentation fault
The first line of the output results from printing the data in the local object name. Then, a copy of name is dynamically allocated; the name_copy_p pointer stores the address of the copy. Printing the data in the copy produces the second line of the output. So far, everything is fine. We can even print the contents of name again ? see the third line of the output.
Next, the SimpleString object pointed to by name_copy_p is deleted. Our modified destructor will free the memory buffer (pointed to by the data_p_ member) being used by the object. The original name object still exists, however, so we should be able to continue using it.
Unfortunately, when we try to use name again (the fourth line in the output) something goes terribly wrong. The data has clearly been damaged; it even causes the program to crash (as shown in the last line of the output). Somehow, deleting a copy of name has seriously broken the original!
This code exhibits a classic dangling reference. To understand how this happened, it is helpful to review the key member functions that every C++ class is required to have. The following list describes these methods.
- Constructor
- Initializes the object during creation. Usually involves allocating resources. More on constructors here.
- Copy Constructor
- A very special constructor, used to create an object that is a copy of an existing object. It is declared like this:
SimpleString( const SimpleString& <em>original</em> );
- Assignment Operator
- Assigns one fully constructed object to another fully constructed object. Declared like this:
SimpleString& operator=( const SimpleString& <em>right_hand_side</em> );
- Destructor
- Cleans up the object’s internals just prior to deletion. Usually involves freeing up resources.
The most important thing to realize about the methods just listed is that the C++ compiler will generate default versions of them if you do not provide your own. The copy constructor and the assignment operator are particularly easy to forget; unfortunately, the defaults often do not do what you want.
The default copy constructor and assignment operator make a simple, shallow copy of every data member. In the A Common Dangling Reference example, this means that the data_p_ pointer inside of the object stored at name_copy_p will now point to the same chunk of memory as name’s data_p_. No attempt is made to allocate more memory and make a deep copy of the data.
When delete name_copy_p; is executed, the SimpleString destructor is called; it frees the memory pointed to by data_p_. Unfortunately, this memory is now being shared with the original object, name. Now, name’s data_p_ points at deallocated memory.
In general, three basic strategies are available to deal with the fact that compiler-generated copy constructors and assignment operators are so often dangerously wrong. These are shown in the following list. Subsequent articles in this series will discuss all three approaches in detail.
- Write your own copy constructors and assignment operators that will work correctly with your classes.
- Disable copying and assignment altogether by making the copy constructor and assignment operator private.
- Modify your classes so that the default copy constructor and assignment operator are correct (by using member objects instead of dynamic allocation, or certain types of smart pointers such as the shared_ptr from Boost.org).
Having closely examined a classic sequence of memory errors, let’s now look at the other common ways in which such errors can be introduced into a C++ program.
By George Belotsky Initially published on Linux DevCenter (http://www.linuxdevcenter.com/)
Examples of Common C++ Memory Errors
In addition to the classic case covered previously, there are several more aspects of C++ programming that tend to cause memory errors. The following set of tables describes these situations. Note that the SimpleString used here is the corrected version, rather than the broken one used to illustrate dangling references. Article three in this series will discuss the corrected version of SimpleString in detail.
Errors in Function (or Method) Calls and Returns
Error: Returning a Reference (or Pointer) to a Local Object
The local object is destroyed when the function returns. In general, anything inside curly braces is a scope; if you define a local object (i.e. non- static) inside of a scope (as shown in the example), it no longer exists after the closing curly brace of that scope.
Result: dangling reference.
Example:
//--- SimpleString& generate_string() { SimpleString localstr("I am a local object!"); //... maybe do some processing here ... return localstr; //As soon as this function //returns, "localstr" is //destroyed. } //---
Returning a const Reference Parameter by const Reference
The C++ compiler is allowed to create an unnamed temporary object for any const reference parameter. After all, you promise not to change the parameter when you declare it const. Unfortunately, the unnamed temporary goes out of scope as soon as control leaves the function, so if you return a const reference parameter by const reference, a dangling reference can result.
Result: dangling reference.
//--- const SimpleString& examine_string(const SimpleString& input) { //... maybe do some processing here ... return input; //If "input" refers to a temporary //object, that object will be //destroyed as soon as this //function returns. } //---
Passing an Object by Value
When a function parameter is an object, rather than a pointer or a reference to one, then the argument (supplied during a call to the function) for that parameter is passed by value. The compiler will make a copy of the argument (the copy constructor will be invoked) in order to generate the function call. If the argument’s type is a derived class of the parameter, then the famous object slicing problem occurs. Any derived class functionality ? including overridden virtual methods ? is simply thrown away. The function gets what it asked for: its own object of the exact class that was specified in the declaration.
While all of this makes perfect sense to the compiler, it is often counterintuitive to the programmer. When you pass an object of the derived class, you expect the overridden virtual methods to be called. After all, this is what virtual methods are for. Unfortunately, it won’t work like that if a derived object is passed by value.
Result: derived parts are “sliced off.” Depending on your design, there may be no memory-related issues, but the slicing problem is important enough to be mentioned here.
//--- //This function slices objects. void the_slicer(Base arg) { arg.example(); } //--- //--- Base base; Derived derived; cout << "Calling 'the_slicer' on a Base" << endl; the_slicer(base); //O.K. cout << endl; cout << "Calling 'the_slicer' on a Derived" << endl; the_slicer(derived); // Slices "derived"; // this is rarely what // you want. //---
Returning a Reference to a Dynamically Allocated Object
Your callers are highly unlikely to take an address of the reference and deallocate the object, especially if the return value is used inside of an expression instead of being immediately saved into a variable.
Result: memory leak.
//--- SimpleString& xform_string_copy(const SimpleString& input) { SimpleString *xformed_p = new SimpleString("I will probably be leaked!"); //... maybe do some processing here ... return *xformed_p; //Callers are highly unlikely //to ever free this object. } //---
By George Belotsky Initially published on Linux DevCenter (http://www.linuxdevcenter.com/)
Errors when Defining Methods in Classes
C++ Default Methods
You must make sure that the default methods that C++ generates work well with your design. The default copy constructor and assignment operator tend to cause the most trouble to programmers. This was extensively covered previously.
Result: memory leak and/or dangling reference.
The Non-Virtual Destructor
If you intend others to inherit from your class, you must declare its destructor virtual. Otherwise, the derived class’ destructor may not be called in circumstances such as the one shown in the example.
Result: memory leak; possibly other problems.
//--- //NVDBase has a non-virtual destructor. NVDBase* base_p = new NVDBase; NVDDerived *derived_p = new NVDDerived; //A base-type pointer to a derived object: //an essential feature in C++. NVDBase* btd_p = new NVDDerived; delete base_p; //O.K. delete derived_p; //O.K. delete btd_p; //Error! Will call only NVDBase's //destructor, even though "btd_p" //points at an NVDDerived! The //object will not be properly //destructed. //---
Errors in Handling of Allocated Memory or Objects
Using Arrays Polymorphically
The ability to access a derived object through a reference or pointer of the base type is central to C++. It allows an important kind of polymorphism (literally, an ability to assume different forms) in which virtual method calls are made based on the actual (fully derived) type of the object, even though the pointer or reference is of the base type.
Unfortunately, this sort of polymorphism does not work with arrays of classes. C++ inherits its arrays from C ? there is basically no difference between an array and a pointer. As you index into an array of derived objects via a base-type pointer, the compiler is happily doing the pointer arithmetic using the size of the base class. The derived class, however, is almost certainly larger than the base class, due to extra members that have been added. Thus, the returned data is rarely a single valid object, but rather pieces of two adjacent ones.
Result: wild pointer (recall that dangling references are a special case of wild pointer).
//--- void process_array(Base* array, unsigned len) { for (int i = 0; i < len; ++i) { array[i].example(); } } //--- //--- Base array_of_base[3]; Derived array_of_derived[3]; //This works as expected. process_array(array_of_base,3); cout << endl; //This is a disaster! process_array(array_of_derived,3); //---
Mistakes Using Casts
Casts have been likened to the infamous goto. This is not completely fair, but nevertheless, casts are rarely required in a well-designed C++ program. A cast will, for example, allow you to convert between two pointers to completely unrelated classes. This produces a wild pointer (recall that dangling references are a type of wild pointer). If you decide to use a cast, make it a very specific, limited part of your program ? preferably hidden away in the internal details of a class. Also, try to use the C++ style casts in preference to the older C style casts. The former come in several varieties, each capable only of a particular kind of conversion, generally making them far safer than the indiscriminate C casts.
Result: wild pointer.
//--- Base base; Base *base_p = &base; //Here, the programmer mistakenly believes that //NotDerived is derived from Base, and that "base_p" //points to a NotDerived object (which would be //perfectly legal if, in fact, NotDerived *was* //derived from Base). //Old C cast is indiscriminate. NotDerived* wild_p = (NotDerived*)base_p; cout << "Value of 'wild_p' is now: " << wild_p << endl; //This will not be soothing at all! wild_p->soothe_me(); cout << endl; //The C++ dynamic_cast is specifically for //"casting down" the class hierarchy. wild_p = dynamic_cast<NotDerived*>(base_p); //The pointer "base_p" does *not* point at a //NotDerived object; dynamic_cast returns a //NULL pointer. cout << "Value of 'wild_p' is now: " << wild_p << endl; //At least the program will just crash immediately, //instead of doing something crazy. A NULL pointer //is much better than a wild pointer! wild_p->soothe_me(); //---
Bitwise Copying of Objects
Mechanisms such as the memcpy function simply copy memory bit by bit. Not all objects, however, can be copied this way without damage. A C++ object is not a piece of dead data; it is a living, intelligent collection of data and functions. When an object is copied, for example, its copy constructor might need to allocate memory, notify other objects, etc. A bitwise copy circumvents these kinds of operations. This results in a seriously broken copy, in many cases.
Besides memcpy, other bit-by-bit copies can result from realloc (it makes such copies when it reallocates memory). An especially easy mistake to make is with variable argument lists. Passing a C++ object using a variable argument list results in a bitwise copy, and is therefore very dangerous (although passing pointers to objects will work, as long as you are mindful of the fact that va_arg uses casts).
Result: memory leaks and dangling references are both possible, as well as other problems.
//--- //Get multiple strings via a variable argument //list, and print them out. void print_strings(unsigned num ...) { va_list narg_p; va_start (narg_p, num); for (unsigned i = 0; i < num; ++i) { SimpleString str = va_arg(narg_p, SimpleString); cout << str.to_cstr() << " "; } cout << endl; } //--- //--- SimpleString first("one"); SimpleString second("two"); //Print out the SimpleStrins directly, //just to show that everything is O.K. cout << first.to_cstr() << " " << second.to_cstr() << endl; //Causes bitwise copies of the //objects -- serious error. print_strings(2,first,second); //We may or may not get here -- //it all depends on luck! cout << first.to_cstr() << " " << second.to_cstr() << endl; //---
Deallocation of Memory That Was Not Dynamically Allocated
It is important to make sure that any memory you free has, in fact, been dynamically allocated. Actions such as freeing local objects or deallocating memory more than once will be disastrous to your program. This also applies to using delete this; inside of a method of your class ? you must indeed be sure that the object has been dynamically allocated before you try to delete it.
Result: memory leaks and dangling references, as well as corruption of operating system data structures.
//--- SimpleString str("I am a local object!"); SimpleString* str_p = &str; //O.K. to use, like any other pointer. cout << str_p->to_cstr() << endl; //The object pointed to by "str_p" was not //dynamically allocated; you should *never* //do the following. delete str_p; //---
Mismatched Method of Allocation and Deallocation
Here is a list of common, matched allocation and deallocation methods.
new | delete |
new [] | delete [] |
malloc() | free() |
It is a serious error to allocate with one method, and then use something other than the corresponding deallocation method to release the memory. In addition, note that malloc and free are not recommended in C++ ? they are not typesafe, and do not call constructors and destructors.
You should also never call an object’s destructor directly. The only exception to this is when you allocate the object using the placement new syntax (e.g. new (ptr_to_allocated_mem) MyClass;). In that case, you should call the object’s destructor when you are finished using it, especially before freeing the memory pointed to by pointer_to_allocated_mem (how you free the memory depends on how you allocated it in the first place).
Result: memory leaks and corruption of operating system data structures.
//--- //Allocate via "new []" -- the array new. SimpleString* str_pa = new SimpleString[10]; //A common but serious error -- you must use the //array delete (i.e. "delete [] str_pa;") here. delete str_pa; //---
By George Belotsky Initially published on Linux DevCenter (http://www.linuxdevcenter.com/)
Errors Related to Exceptions
Partially Constructed Objects
When an exception prevents a constructor from running to completion, C++ will not call the destructor of the partially constructed object. Member objects, however, will still be properly destructed. This is not to say that exceptions are to be avoided in constructors. In fact, throwing an exception is usually the best way to indicate that a constructor has failed. The alternative is leaving around a broken object, which the caller must check for validity.
While you should often allow exceptions to propagate out of a constructor, it is important to remember that the destructor will never be called in such cases, so you are responsible for cleaning up the partially constructed object (e.g. by catching and then rethrowing the exception, using smart pointers, etc.).
Result: memory leak.
//--- class CtorThrow { public: CtorThrow(); ~CtorThrow(); //N.B. non-virtual, //not meant for subclassing. private: Base* first_p_; Base* second_p_; Derived member_obj_; //See the Training Wheels Class //for an explanation of these declarations. CtorThrow(const CtorThrow&); CtorThrow& operator=(const CtorThrow&); }; CtorThrow::CtorThrow() : first_p_(0), second_p_(0), member_obj_() { first_p_ = new Base; //Could also call a function/method that throws; //in any case, "first_p_" is leaked. throw SimpleString("Exception!"); second_p_ = new Base; } //Destructor will not be called when exception //leaves the constructor. CtorThrow::~CtorThrow() { cout << "Destroying CthorThrow" << endl; delete first_p_; delete second_p_; } //---
Exceptions Leaving a Destructor
If a function throws an exception, that exception may be caught by the caller, or the caller’s caller, etc. This flexibility is what makes error handling via exceptions into such a powerful tool. In order to propagate through your program, however, an exception needs to leave one scope after another ? almost as if one function after another were returning from a chain of nested of calls. This process is called stack unwinding .
As a propagating exception unwinds the stack, it encounters local objects. Just like in a return from a function, these local objects must be properly destroyed. This is not a problem unless an object’s destructor throws an exception. Because there is no general way to decide which exception to continue processing (the currently active one or the the new one thrown by the destructor), C++ simply calls the global terminate function. By default, terminate calls abort, which abruptly ends the program. In consequence, local objects are not properly destructed. While the operating system should reclaim the memory when your program terminates, any complex resource that requires your destructors to run (e.g., a database connection) will not be properly cleaned up.
Another consequence of an exception leaving a destructor is that the destructor itself does not finish its work. This could lead to memory leaks. Your destructor does not necessarily have to throw an exception itself for the problem to happen. It is far more common that something else called by the destructor throws the exception. In general, it is best if you make sure that exceptions never propagate out of your destructors under any circumstances (even if your compiler implements the Boolean uncaught_exception function, which can be used to test if an exception is already in progress).
Result: memory leak (destructor does not run to completion); local objects will not be properly destructed if another exception is active. Various resource leaks and state inconsistency are therefore possible.
//--- //When DtorThrow throws an exception in in its //destructor, but no other exception is active, //all other objects are properly destroyed. try { cout << "Only one exception ..." << endl; Base obj1; Derived obj2; DtorThrow obj3; } catch(...) {//Catch everything. cout << "Caught an exception" << endl; } cout << endl; //When another exception is active, and DtorThrow //throws an exception in its destructor, the other //objects are *not* properly destroyed. try { cout << "Exception during another exception ..." << endl; Base obj1; Derived obj2; DtorThrow obj3; throw SimpleString("Exception!"); } catch (...) {//Catch everything; //we never get here. cout << "Caught an exception" << endl; } //---
Improper Throwing
When throwing exceptions, it is important to remember that the object being thrown is always copied. Hence, it is safe to throw local objects, but throwing dynamically allocated objects by value or by reference will cause them to be leaked. Copying is always based on the static type of the object, so if you have a base-type reference to an object of a derived class, and you decide to throw that reference, an object of the base type will be thrown. This is another variant of the object slicing problem covered earlier.
A more subtle slicing error occurs when rethrowing exceptions. If you want to rethrow the exact same object that you got in your catch clause, simply use throw; ? not something like throw arg;. The latter will construct a new copy of the object, which will slice off any derived parts of the original.
You also need to make sure that the copy constructor of the class that you are throwing will not cause dangling references. It is generally not recommended to throw exceptions by pointer; in these situations, only the pointer itself, rather than the actual object, is copied. Thus, throwing a pointer to a local object is never safe. On the other hand, throwing a non-local object by pointer raises the question of whether it needs to be deallocated or not, and what is responsible for the deallocation.
Result: object slicing, dangling references, and memory leaks are all possible.
//--- //Outer try block. try { //Inner try block. try { throw Derived(); } //Catch by reference -- won't slice. catch (Base& ex) { ex.example(); //O.K. //Rethrow ... throw ex; //Mistake -- slices! //Should just use "throw;". } //END Inner try block. } //Should be fine, but ... catch(Base& ex) { ex.example(); //... not what we expected! } //---
Improper Catching
Improper catching of exceptions can also lead to object slicing. As you might have guessed, catching by value will slice. The order of the catch clauses matters, too; always list the catch for an exception of a derived class before the catch of its base class. The exception mechanism uses the first catch clause that works, so listing base classes up front will always shadow the derived classes.
Result: object slicing; memory-related errors and other problems are possible if a base-class catch shadows a derived-class catch, thus preventing the latter from taking actions specific to a derived-type exception.
//--- try { throw Derived(); } //This not only shadows the Derived catch //clause -- it slices, too! catch (Base ex) { cout << "caught a Base" << endl; ex.example(); //Sliced! } //We never get here! catch (Derived ex) { cout << "caught a Derived" << endl; ex.example(); } //---
The Way Forward
This article has focused on finding the right role for C++ amongst today’s other popular languages, and on understanding its most difficult aspect: memory management. The tables of common memory-related errors presented here can be used as a handy reference, to find and avoid such errors in your own code. Subsequent articles of the series will continue to discuss C++ memory management in greater detail.
The second article will be devoted to describing the nature of the C++ memory management mechanism, so that you can begin to apply it creatively in your designs. After that, the third article will present a series of specific techniques that you can use as building blocks in your programs.
C++ memory management is an enormously useful tool for creating elegant software. Having gained a clear awareness of its dangers, you are now ready to understand its benefits. Enabling you to do so is, ultimately, the purpose of this series of articles.
By George Belotsky Initially published on Linux DevCenter (http://www.linuxdevcenter.com/)