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.
SimpleString -- the Classic Example ContinuedThe 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.
|
Related Reading Secure Programming Cookbook for C and C++ |
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 ***
//*** 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.
|
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 |
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.
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.

Figure 1. Assignment semantics of ordinary pointers

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.
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.
|
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.
References are far safer than pointers; a function with reference parameters cannot corrupt those parameters to point to some invalid location—something that is always a danger with pointers.
References are very efficient as parameters, because the actual objects are not copied, as they must be if passed by value.
References have a very simple, pleasant syntax, both when writing a function call and when working with the parameter inside the function itself.
References do not exhibit the object slicing (covered in the first article) problem. Virtual functions work as intended.
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.
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.
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!
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++.
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.
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.