Edsger Dijkstra's famous 1968 letter to the CACM, best known as "Go To Statement Considered Harmful," argued that statements such as
goto, despite their power, had hazards that programmers would do better to avoid. This letter started a programming revolution, to the point that today, transfers of control are almost always part of more restrictive control structures such as
C++ destructors have their own hazards, with nature and causes that are analogous to those of the
goto statement. This article argues that programmers should seriously consider restricting the use of destructors to data structures that are intended to avoid those hazards.
Dijkstra and Structured Programming
Edsger Dijkstra's 1968 letter to the Communications of the ACM (CACM) was originally entitled "A Case Against the GO TO Statement." The CACM retitled it "Go To Statement Considered Harmful." The document is generally credited with starting the Structured Programming revolution, which so affected how developers program that languages today such as Python and Java do not even have
When asked why
goto statements are harmful, most programmers will say something like "because they make programs hard to understand." Press harder, and you may well hear something like "I don't really know, but that's what I was taught." For that reason, I'd like to summarize Dijkstra's arguments.
People understand static relationships better than dynamic relationships because the latter requires their understanding to change constantly. Therefore, it is easier to understand how a program works if we can express that understanding in terms of properties of the program, rather than of the program's execution. This former understanding stays the same until someone rewrites the program, but the latter might have to change each time we run the program.
To understand a program's properties means to be able to point to a place in the program and think: "Whenever the program gets to this point, certain conditions are true." For example:
if (n < 0) n = 0;
n is a variable of a built-in numeric type, we know that after this code,
n is nonnegative.
Suppose we rewrite this fragment:
if (n >= 0) goto nonneg; n = 0; nonneg: ;
In theory, this rewrite should have the same effect as the original. However, rewriting has changed something important: It has opened the possibility of transferring control to nonneg from anywhere else in the program. In other words, even though rewriting the fragment may not have changed its behavior, we no longer know that when control reaches the statement after this fragment,
n is nonnegative.
Another way in which
goto statements make it hard to understand programs statically is by making it hard to describe how much progress a program has made during its execution. If a program includes only limited control structures such as
while, then we can talk about which path each
if has taken and how many times each
while has executed, and build up from this information a complete picture of the program's execution history. Such a picture lets us understand programs by making claims such as "The number of iterations of this loop is the same as the number of input records so far."
goto statements complicate keeping track of the program's execution, because there is no limit to how much executing a single
goto can change the future path through the program. As a result, the program's execution history becomes much harder to describe.
Both of these hazards of
goto statements affect our ability to think about programs, particularly our ability to understand a program's dynamic behavior by making static claims about it.
Allocating Resources Is Safer Than Freeing Them
Now let's look at pointers. C++ programmers learn that pointers are dangerous because they might not point to valid memory but they don't learn why pointers wind up not pointing to valid memory.
There are two fundamental reasons that pointers might be invalid. First, the pointer might never have been initialized. This problem is not unique to pointers: Any variable of a built-in type can have a garbage value if the programmer forgets to initialize it. The unique hazard of pointers happens this way:
- A program allocates dynamic memory and uses its address to initialize a pointer.
- The program frees the memory, leaving the pointer with its previous now invalid value.
- The program tries to use the now unallocated memory to which the pointer points, and crashes.
It is important to realize that (1) is safe! It is not until (2) that there is any possibility of a problem. The hazard comes not in allocating memory, but in freeing it. Once a pointer points at correctly allocated memory, it will continue to do so unless that memory is freed. Of course, unless we have more memory than we need, we must free it eventually; when we do so, we must be sure that there are no other pointers to that memory.
This kind of assurance is similar to the assurance that we lost when we added a
goto statement to our earlier program. In effect, freeing memory is like transferring control, and pointers are like labels: When a program frees memory (transfers control), we must make certain that every pointer (label) that might inappropriately point to that memory (be the target of the transfer) does not do so in this particular case.