Andrew Koenig is a former AT&T researcher and programmer. He is the author of C Traps and Pitfalls and, with Barbara, coauthor of Ruminations on C++ and Accelerated C++. Barbara Moo is an independent consultant with 20 years of experience in the software field.
A writer met a photographer at a party and said: "I've seen your work at a gallery; it's great. You must have a superb camera!" The photographer replied: "I've read your latest novel; it's great. You must have a superb typewriter!"
Somehow, the notion that a superb typewriter is enough to make a great novel seems more absurd than the notion that a superb camera is enough to make a great photograph. That's why the joke is funny: The punch line hits us with just how ridiculous it is to credit a camera for a great photograph.
In reality, of course, it takes a great writer or photographer to produce great literature or photography. Good tools help: A good typist can write much faster than anyone can write by hand, and there are no photographs at all without a camera. Nevertheless, tools are not enough by themselves. If you think that dropping a few grand on photo equipment will make you the next Ansel Adams, you will be disappointed.
This reality pervades the teaching of writing and photography. Writers' workshops do not spend time discussing typewriters and word processors. Instead, they give writers the opportunity for other writersusually more experiencedto analyze their work. Photography workshopsexcept for the ones that are really disguised sales pitches for equipmentusually spend much less time talking about cameras and lenses than they spend on analyzing photographs to understand what makes them good and how to improve them.
So why is it so easy to believe that a superb camera will guarantee great pictures? We suspect the reason is that photography depends intimately on technology in a way that writing does not, and without understanding the technology thoroughly, it is easy to overestimate the technology's contribution to the final product. No camera means no photographsso why shouldn't a superb camera mean great photographs?
Like photography, programming depends on technologyso we are similarly tempted to believe that the technology will do more for us than it really does. For example, one easy way to recognize a beginning programmer is that a beginner's first step toward solving a programming problem is often to look at language features. How often have you heard a beginner say something such as, "I can solve this problem by using inheritance, right?"
More experienced programmers will begin by thinking about algorithms and data structures that might solve the problem, rather than starting with the language features that might implement those algorithms and data structures. Programmers with much more experience will begin by looking carefully at the problem and understanding exactly what properties the solution needs to have. Might it have to change in the future? In what ways? What interface should it have? Once we think we have a solution, how do we test it? Are any parts intrinsically difficult to test? And so on.
Experienced programmers no longer need to think about the details of their tools, any more than experienced photographers need to think about the details of their cameras. Indeed, it is reasonable to define "advanced" as what you learn after you've mastered the basics to the point that you don't have to think about them any more.
Universal Prerequisites
The foregoing discussion implies that programmers should not think about advanced anything until they understand thoroughly the tools that they use. This implication does not require C++ programmers to understand everything there is to know about C++but it does require that they understand thoroughly the parts of C++ that they use in their programs. This understanding extends not only to the language itself, but also to the Standard Library and the requirements that the library imposes on the programs that use it. Unfortunately, you need to look no further than the Usenet comp.lang.c++ newsgroup to see how widespread the lack of understanding is (even of simple ideas).
For example, every few months, someone shows a program fragment similar to:
int n = 4; int k = n + n++; std::cout << k << std::endl;
and complains that the program prints 8 when run on one compiler and 9 when run on another. Does this discrepancy mean that one compiler is broken?
A common early response is that when we write n+n++, the compiler is free to evaluate the operands of the + in either order, which means that the implementation can decide whether to evaluate the n on the left side of the + before or after the n++ on the right side has incremented n.
Plausible though it may be, this response is wrong. What the C++ Standard actually says is that if a program fetches and modifies a single object between two sequence points in a single expression, the implementation can do anything it likesunless the program fetches the value only once and uses the value to determine the object's new value. By itself, the subexpression n++ meets this requirement: It fetches the value of n to determine the new value of n. However, there is no sequence point in n+n++, so this expression fetches n twice in addition to modifying it. The effect of the expression is therefore undefined, and the Standard imposes no requirements on how implementations should handle it. As an extreme example, a Standard-conforming implementation could execute such a program by deleting all of the user's files.
Every C++ programmer should avoid undefined behavior because there is no assurance that a program that evokes such behavior will do anything useful. If it does do something useful, it is only by coincidence, perhaps because a particular implementation chooses to define the behavior of such programs. By avoiding undefined behavior, we avoid having to worry about whether moving our program to a new machine, or installing a new release of our compiler, will make our programs stop workingor more accurately, stop appearing to work. Nevertheless, the responses to the claim of undefined behavior typically include statements along the lines of, "No it isn't!" or, "But it works on my compiler!" Such misconceptions are serious obstacles to mastery.
Another misconception that shows up regularly involves the Standard Library's conventions for comparison functions used by associative containers. The typical problem comes when someone defines a data structure to represent x-y coordinates:
struct Point { int x, y; };
and then tries to create a set<Point>. Doing so requires defining an appropriate order relation; a common mistake is to write something like:
bool operator<(const Point& p, const Point& q) { return p.x < q.x && p.y < q.y; }
This version of operator< defines a point p as "less than" a point q if and only if p's x and y coordinates are both less than q's respective coordinates.
This definition is wrong. The reason is easy to explain informally: If p.x < q.x and q.y < p.y (note the reversed order of p and q), then neither p nor q will be considered "less than" the other; yet it doesn't make sense to treat them as equal. Under this definition, for example, (3,4) would be considered "equal" to (2,6) because 2<3 and 4<6. For similar reasons, (2,6) would be considered "equal" to (4,5). However, (3,4) is "less than" (4,5), a result that is inconsistent with any plausible notion of equality. If (3,4) is "less than" (4,5) and "equal to" (2,6), then you should reasonably expect (2,6) to be "less than" (4,5).
The right way to define operator< is easyif you've seen it before:
// Is p less than q in dictionary order? bool operator<(const Point& p, const Point& q) { return p.x < q.x || !(q.x < p.x) && p.y < q.y; }
This function implements the notion of dictionary order. If p.x is less than q.x, we consider p to be less than q. Otherwise, we might still view p as less than q if p.x is equal to q.x and p.y is less than q.y. To check for this alternative, we check first whether p.x and q.x are equal. Wishing to use only < for our comparisons, we do this test indirectly as !(q.x < p.x), which we can view as q.x >= p.x or, alternatively, as p.x <= q.x. Because we already know that p.x < q.x is false, the only way that this test will succeed is if p.x and q.x are equal. If p.x and q.x are equal, the result of the comparison is the result of comparing p.y and q.y.
This function has several noteworthy subtleties. Most important is the need to write it at all, which comes from the important realization that the original version doesn't meet the library's requirements for how the < operator should behave. Next is the notion of dictionary order and how to implement it. Then comes the tactic of using only the < operation in the implementation. This tactic is not important for integers, but is a good habit to get into for writing generic programs in the future. Lastand leastis realizing that && binds more tightly than ||, so that our program is equivalent to:
// Is p less than q in dictionary order? bool operator<(const Point& p, const Point& q) { return p.x < q.x || (!(q.x < p.x) && p.y < q.y); }
Some people may argue that we should have included the parentheses. Others may say that we should write the function this way:
// Is p less than q in dictionary order? bool operator<(const Point& p, const Point& q) { if (p.x < q.x) return true; if (q.x < p.x) return false; return p.y < q.y; }
We don't think it's a major issue one way or the other. What is important is realizing that we need to implement dictionary order, and doing so in a way that uses only <.
We are belaboring this example because we think it shows an important point: Understanding how to write comparison functions such as this one is a fundamental part of using the C++ associative containers and many of the algorithms, yet that understanding does not depend on arcane language features. In other words, in order to use C++ and its libraries effectively, one must understand a number of ideas that are not directly connected to language features.
There are other ideas in C++ that it is also essential to understand:
- The relationship between constructors, destructors, and copy constructors, especially in the context of objects that behave like values.
- The relationship between containers and iterators, especially in the context of generic programs.
- The relationship between inheritance and pointers, along with techniques to conceal pointers in handles of various kinds.
Each of these ideas is closely connected to particular language features, but the feature is only a small part of the idea's intellectual content. For example, most programs that care about iterators and iterator categories will use templates, but understanding iterator categories goes far beyond understanding the mechanics of how to use templates. To understand iterators, one must not only understand templates but also the strategy with which the library uses them to define iterators and iterator categories. If you like, understanding how templates work is like understanding how to use a camera; understanding how to use iterator categories is like understanding how to compose a photograph.
Summary
We contend that if the notion of "advanced C++" is to mean anything useful, it should refer to what you learn after mastering the basics. These basics include much more than just the rules of the language and library. Some language features often considered "advanced," such as pointers to members, are merely specialized tools that are easily learned as neededgiven a thorough understanding of the basicsand conceal no unusual intellectual difficulty. In contrast, if you don't understand the Rule of Three (that is, "If your class needs a destructor, it almost certainly also needs a copy constructor and an assignment operator"), you will get into trouble nearly every time you try to define a class with objects that act like values.
Many of the notions that today we consider as part of the basics were once considered advanced. For example, when Alex Stepanov and his colleagues first published what they called the Standard Template Library, we were uncertain about whether C++ programmers would ever learn to program in the style that the library implied. Several years later, we wrote Accelerated C++, an introductory book that uses that library as an integral part of its approach. Ideas that we first thought might be too advanced to ever be widely adopted became part of how we introduce beginners to the language.
We have already mentioned some other ideas once considered advanced that we now think should be part of the basics. Other such ideas include RAII (Resource Acquisition Is Initialization) and the swap idiom for exception-safe state changes to containers. Most of these ideas have several common aspects:
- They are programming techniques or parts of a library, rather than direct parts of the language.
- They were once considered too advanced for general use.
- They are so widely useful today that they deserve to be part of almost every C++ programmer's toolkit.
These ideas make programs easier to write and understandeven if the ideas are implemented in ways that require additional experience to understand. For example, beginning C++ programmers can use the Standard Library containers and algorithms to advantage, even though it takes a great deal of experience to understand in detail how those containers and algorithms are implemented. Treating such ideas as part of the fundamentals has another advantage: When it comes time to learn about how to implement such notions, the student already has the benefit of knowing how to use them.
We believe that the C++ community should stop using the term "advanced" to refer to such notions, and that future teaching strategies should include them as part of the basic curriculum. We should reserve "advanced" to describe ideas that may shape future C++ programming styles, but that are not yet universally useful.
We do not expect an advanced writing workshop to teach how to use arcane word-processor features, nor do we expect an advanced photography workshop to spend much time on equipment. Instead, we expect that we will be assumed to have learned already how to use our tools, and that we will learn how to use those tools in ways that are not evident to beginners.
We should use the word "advanced" similarly in our own community.