One reason that C++ became popular is its familiarity to C programmers. Especially in C++'s early days, this familiarity made it easy for C programmers to learn C++ as an addition to what they already knew. As a result, most people who taught and wrote about C++ at that time assumed C as a prerequisiteor taught the fundamentals of C before covering the rest of C++.
In the past few years, it has become easier to learn C++ without having to learn C first. One important reason is that several essential parts of the C language and Standard Library have C++ counterparts that have become universally available as part of the C++ Standard. These C++ counterparts are easier to use and fit better with the rest of C++ than do the C versions. Accordingly, programmers who wish to use C++ effectively have a good reason not to bother with those parts of C unless they need them for other purposes.
These C++ facilities were not part of the earliest C++ implementations. When the C++ Standard was published in 1998, most implementations had not yet caught up to the standard. This fact is not surprising; usually it takes at least four years between the decision to make a major change in a software system and the time that the change becomes widely available to users. Accordingly, when we wrote Accelerated C++ (published in 2000, two years after the C++ Standard), we expected that although the C++ Standard Library would be generally available, some early readers would have to change the book's programs slightly to deal with their implementations' limitations. We also expected that any such difficulty would go away over the next couple of years. It is now nearly five years since the C++ Standard's publication, and, as we expected, the programs in Accelerated C++ work just fine on current C++ implementations. In short, Standard C++ has entered the mainstream, so it is time for us to start taking advantage of it as a matter of course.
In Accelerated C++, we tried to follow three principles consistently:
- Explain ideas in an order that will let the reader solve useful problems as quickly as possible, rather than grouping similar features together.
- Explain how to use library facilities that already exist before explaining how to implement one's own libraries.
- Use the standard C++'s library facilities instead of their C counterparts.
At the time, these three principles were radically different from the usual ways of teaching C++. Even today, we believe that reasonable people can disagree about the first two principles.
The third principle is a different matter. Now that good implementations of the Standard Library are widely available, we believe that it is time for everyone who sets out to teach, learn, or use C++ to prefer key parts of the C++ Library to their C counterparts. Programmers who wish to learn more about C can do so later.
In this context, we consider four parts of the C++ Library to be particularly important: strings, typed input-output, vectors, and algorithms. The rest of this article will explain these notions, and why we think they are important.
Strings
Of all the ways in which the C++ Library makes programming easier, the string class probably offers the biggest payoff for the least effort. A data structure such as
struct Person { std::string name, address; };offers an easy way to store character data that might vary in length, without having to worry about memory allocation, bounds checking, and all of the other problems that come with C-style string handling. In particular, using the string class makes it much easier to write programs that avoid the "buffer overrun'' bugs that are the source of so many recent, heavily publicized security problems.
We would have thought by now that the security benefits alone of using the standard string class instead of dealing explicitly with character arrays would be so obvious that there would be no need for us to advocate using strings instead of character arrays. Nevertheless, we still see programs that use data structures such as
struct Person { char name[30], address[50]; };People who use such data structures take a host of problems on their shoulders. For example, how should a program treat a person with a name longer than 30 characters? All too many programs assume that such a name will never appear, and then crash when it comes along. For that matter, how should a program treat a person with a name that is exactly 30 characters long, noting that the null character at the end of C-style strings usually makes them take up one character more than their nominal length?
To make these questions more concrete, suppose p is an object of type Person. Now imagine that you have just read a name into a local variable named name, that you wish to copy this name into p.name, and that you do not know how long the name in name is. If you are using the C version of struct Person, there are several ways of solving this problem, all of them flawed. For example, you might write
strcpy(p.name, name);but if name is too long, this call will crash. Alternatively, you can defend against the crash by writing:
strncpy(p.name, name, sizeof(p.name));This version won't crash even if name is too long, but it will omit the null terminator from the end of p.name, possibly provoking a crash later. Of course, this call to strncpy will also quietly truncate a name that is too long, a side effect that may or may not be desirable.
It is possible to defend against the delayed crash by writing:
strncpy(p.name, name, sizeof(p.name)); p.name[sizeof(p.name)-1] = '\0';This revision forces the last character of p.name to be a null character. If the name in name is exactly 29 characters long, this null character will act as a terminator; otherwise it will have no effect. Would you have thought of the need to put this null character there if we hadn't pointed it out to you?
In contrast, if we are using the C++ version of struct Person, we can write:
p.name = name;For that matter, this code will work regardless of whether name is a string or a null-terminated character array. Virtually every other operation on standard C++ strings is similarly easier and safer than the corresponding operation on C-style strings.
Typed input-output
The classic first C program is one that merely announces its existence on the standard output stream:#include <studio.h> int main() { printf("Hello, world!\n"); return 0; }Even this tiny program conceals two problems that point C++ beginners in the wrong direction. The first problem is that the program suggests that Hello, world! is data to be printed. That suggestion is misleading: The string "Hello, world!\n" is really a format that describes the output. The distinction between format and data is important because some characters, particularly the % character, have special meanings as part of format strings but not as part of data strings. We can make the program less misleading by rewriting it:
#include <stdio.h> int main() { printf("%s\n", "Hello, world!"); return 0; }This revision makes it plain that what we are printing is the string Hello, world!, and that we are following that string by a newline that we are treating as a line terminator, rather than as part of the data. The revised version also reveals the second problem: The printf format strings can contain sequences of characters, each beginning with %, that correspond to the other arguments to printf. The reason that this correspondence is the problem is that the particular characters that follow the % are required to match the type of the corresponding argument.
To see why the type-matching requirement is a problem, consider the following statement:
printf("%d\n", 12.34); /* undefined behavior */This statement tries to write a double value (12.34) with a format string (%d) that demands an int; the effect of doing so is undefined. Implementations are permitted to check whether the relevant part of the format string matches the type of the associated expression, but they are not required to do so. Moreover, such checking is not always feasible at compile time, and the effect at run time of failure to check is anyone's guess.
In contrast, the standard C++ input-output library uses overloading to choose appropriate formatting during compilation. For example, the classical first C++ program,
#include <iostream> int main() { std::cout << "Hello, world!" << std::endl; return 0; }does not use a format string at all. Instead, it uses an output format that is inferred from the fact that "Hello, world!" is a string literal. In that sense, this C++ program expresses more clearly than its C counterpart the idea of printing the string Hello, world! followed by a newline.
From a C++ viewpoint, the need to match format strings with argument types is more than just a matter of run-time safety. For one thing, printf formats do not encompass even such commonly used C++ types as string. Programs that use strings have to go to extra trouble to print them with printf:
string hello = "Hello, world!"; printf("%s\n", hello.c_str());Here, the call to c_str returns a value that is appropriate to pass as an argument to printf. Again, the effect of a program that tries to print hello instead of hello.c_str() is undefined. Programmers who use the native C++ I/O library can avoid this inconvenience:
string hello = "Hello, world!"; std::cout << hello << std::endl;Because the C++ Library overloads << appropriately, the type of hello selects the appropriate output routine, and the programmer does not have to mention the type of hello explicitly in order to print it.
It is even harder to use printf in the presence of templates. In general, objects' types in template code are unknown until the program is compiled, which makes it difficult to choose appropriate characters for format strings as part of writing a program. Indeed, an appropriate format string may not even exist, because there is no way to extend printf format strings to encompass user-defined types.
Because the C++ Library uses overloading to select the format to use, there is no particular problem in adding new types to the ones that already exist; one merely writes new overloaded versions of <<. Moreover, in a statement such as:
std::cout << x;the compiler will select the appropriate << during compilation, without the programmer having to know the type of x as part of writing the program.
Vectors
A question that surfaces frequently on Usenet is "Why doesn't C++ let you say how many elements an array has at run time?" The typical example looks something like this:int n; std::cin >> n; int x[n]; // Why isn't this // legal?The typical answer is that one can write it this way instead:
int n; std::cin >> n; std::vector<int> x(n);This answer is true, but misleading, because in practice, merely being able to allocate an array of a given size is not enough, even if the size doesn't need to be known during compilation. A typical case is when one has a collection of data, and not until after one has read the entire collection that one knows how many elements it has. For example:
std::vector<int> x; int n; while (std::cin >> n) x.push_back(n);Here, the vector x starts out with no elements at all. Each iteration of the loop appends an element to x. When the program reaches the end of the input, all of the input values are in x. Until then, x grows as needed to accommodate the input data.
The library has to do a surprising amount of work to get this kind of allocation right. To see how much work it is, try doing it yourself. We shall dismiss right away the notion of using malloc or free, because these functions allocate raw memory, not C++ objects. Therefore, we shall explore the consequences of using new and delete to allocate dynamically sized arrays. For example, let's suppose we've allocated an array with n elements of type T:
T* array = new T[n];At this point, array points to the initial element of the array that we've allocated. Suppose that we want to cause this array to contain m elements instead. We might do so this way:
T* newarray = new T[m]; std::copy(array, array + std::min(m, n), newarray); delete[] array; array = newarray;The first line of this code allocates the new array, which we want to contain m elements. The second line relies on the fact that when we add an integer x to a pointer to an array element, the result is a pointer to a position x elements further into the array. In other words, array points to an element of an array in this case, element number 0. Therefore, if we add an integer to array, we get a pointer to the corresponding element. So array + std::min(m, n) is a pointer to element number m or element number n of the array, whichever is first. array + std::min(m, n) points one past the last element we wish to copy from the old array to the new array. The call to std::copy copies just those elements.
However, this code has a problem: It's not exception safe. If an exception occurs while std::copy is executing, some of the elements of the array will have been copied and others won't. Moreover, we will have two arrays sitting around, one with array pointing to its initial element and the other with newarray pointing to its initial element. We can make this code behave better in the presence of exceptions by rewriting it this way:
{ T* newarray = new T[m]; try { std::copy(array, array + std::min(m, n), newarray); } catch(...) { delete[] newarray; throw; } delete[] array; array = newarray; }This rewrite offers what is usualy called the strong exception guarantee: Either it completes successfully or it leaves the program in the state it was in before the code started. If the call to new throws an exception, it should be obvious that the state is back where it was. If an exception occurs inside std::copy, then we delete the new array and propagate the exception to the surrounding context. As usual, we assume that destructors and delete statements will not throw exceptions.
If we use the vector class instead, we can reallocate a vector to contain m elements, while offering the strong exception guarantee, simply by calling
v.resize(m);We can imagine reasons why one might eventually want to learn how to deal with memory explicitly, but we feel strongly that there is no reason to do so until after becoming thoroughly familiar with vectors.
Algorithms
Many C Library functions, such as sqrt, are also useful in C++. However, several C functions are intended to offer generic algorithms; the C++ versions of these functions are much more useful. Two such functions that are particularly important to avoid are memset and memcpy. For example, we sometimes see code like this:double x[100], y[100]; // ... memcpy(x, y, sizeof(x));In C, this example is probably the most sensible way to copy the contents of y into x. However, it is an example of very bad technique in C++, because it works only if x and y are of so-called POD typesthat is, that they have no user-defined (or library-defined) constructors or assignment operators. So, for example, if x and y were arrays of strings, this example would fail. A much better way of accomplishing the same thing in C++ is:
std::copy(y, y + 100, x);The first two arguments to std::copy are the beginning and one past the end of the range to copy; the third argument is the beginning of the destination.
Because std::copy takes user-defined assignment into account, this usage would work equally well if x and y were arrays of strings or of other user-defined or library types.
Heeding our earlier advice to use vectors would have simplified the program still further:
vector<double> x, y; // ... x = y;The memset function is particularly problematic because people often use it in ways that are technically illegitimate. For example:
int x[100]; // ... memset(x, 0, sizeof(x));The call to memset sets every byte of the memory that x occupies to zero. On most C++ implementations, doing so will cause the integer elements of x to take on the value zero. However, this behavior is not guaranteed, which makes this program fragment particularly insidious. In effect, such uses of memset cause bugs that no amount of testing can ever be assured of revealing. A much better way to accomplish the same thing is to use the std::fill_n function:
int x[100]; // ... std::fill_n(x, 100, 0);The first argument points to the initial element to be filled, the second argument is the number of elements, and the third is the value to use to fill the range. So, for example, instead of calling std::fill_n to initialize x, we can write
std::fill(x, x + 100, 0);In such contexts, the std::fill function is also often useful. Like std::copy, its first two arguments denote a range; the third argument is the value to use to fill the range.
Another C function to avoid is qsort. To see why, here's an example of what it takes to use it. Suppose, again, that x is an array with 100 elements:
int x[100];To use qsort to sort this array, we would call qsort this way:
qsort(x, 100, sizeof(int), compare);The first argument is a pointer to the initial element of the array to be sorted. The second and third arguments are the number of elements and the size, in bytes, of a single element. The fourth argument is a comparison function, which we must supply.
The comparison function takes pointers to two elements of the array and returns a negative, zero, or positive value, depending on the relative order of the elements. Because qsort is intended to be generic, its arguments can't be pointers to a specific type. In C++, the usual way to deal with generic types is by using templates. However, C doesn't have templates, so qsort can't use templates for genericity. Instead, qsort passes the comparison function two pointers to const void, which we must cast into pointers to the appropriate type. Here is one possible comparison function:
int compare(const void* p1, const void* p2) { int i1 = *(const int*) p1; int i2 = *(const int*) p2; if (i1 < i2) return -1; if (i1 > i2) return 1; return 0; }In contrast, using std::sort to sort this array is much simpler:
std::sort(x, x + 100);Moreover, std::sort can also sort vectors.
We cannot imagine a case in which it is better to use qsort in a C++ program than it is to use std::sort. Indeed, because qsort effectively uses memcpy to copy the elements of the array that it is sorting, there is no reliable way to use qsort to sort an array of strings or of many other user-defined types.
Conclusion
C++ became popular because it offered C programmers a way of programming more abstractly. Because of this fact, most people who set out to teach and learn C++ in its early days began by assuming a knowledge of C++ and by teaching that knowledge to students who did not already have it. As a result, it became customary to introduce people to C++ through C.Now that good implementations of the Standard C++ Library are generally available, the situation has changed. We believe that the most effective way to teach -- or learn -- C++ is to bypass several fundamental parts of C entirely.
Strings and vectors make many kinds of programs dramatically easier to write than do their C counterparts. Moreover, because they relieve the programmer of the burden of keeping track of memory, they make buffer overruns and related security problems much less likely.
The C++ input-output and algorithm libraries make programs easier to write by taking advantage of the C++ language's abstraction facilities. Accordingly, they keep working in the face of user-defined types in ways that the C facilities cannot do.
Arrays, printf, malloc, free, strcpy, strncpy, memcpy, memmove, and qsort all have their place. However, we believe that place should no longer be part of introducing C++ to new programmers, whether or not they already know C.
About the Author
Andrew Koenig retired from AT&T's research division in June, 2003. A programmer for more than 35 years, 17 of them in C++, he has published more than 150 articles about C++ and speaks on the topic worldwide. He is the author of C Traps and Pitfalls and co-author of Ruminations on C++ and Accelerated C++.
Barbara E. Moo is an independent consultant with 20 years' experience in the software field. During her nearly 15 years at AT&T, she worked on one of the first commercial projects ever written in C++, managed the company's first C++ compiler project, and directed the development of AT&T's award-winning WorldNet Internet service business. She is co-author of Ruminations on C++ and Accelerated C++ and lectures worldwide.