Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C++ Made Easier: Programs That Work by Accident


February 2001/C++ Made Easier


Introduction

We often see questions about C++ on Usenet of the form "Does the following statement have a well defined effect?" or, more naively (and more bluntly): "I wrote the following statement and it didn’t do what I expected. What’s wrong with my compiler?"

Sometimes the answer is that the given statement is well defined. More often — particularly in the cases that impel people to ask questions — the answer is that the implementation is permitted to do as it pleases, that different implementations behave in different ways, and that if the program happened to do what the author wanted, it did so only by coincidence.

You might think it strange that a programming language could fail to define a single meaning for every statement that one could ever write. However, there are some usages that would be difficult or inefficient to define, at least on some hardware, and C++ programmers are not always willing to pay the price for such definitions.

For example, what should happen if a program computes n+1 and the value of n happens to be the largest possible integer? It is easy to say the effect should be an integer overflow — but what should be the resulting value?

Suppose that we are trying to implement C++ on a machine that has no mechanism for raising a hardware exception when integer overflow occurs. If the language definition prescribed a particular behavior in case of integer overflow, our implementation would have to generate extra code to check for overflow after every addition. Experience has shown that there are some programmers who would find such overhead unacceptable.

Accordingly, there are some statements with behavior that the C++ language does not define, leaving it to each implementation to decide how the statement should behave. It is particularly important for programmers to be aware of such usages if they wish to write robust, portable code. Otherwise, they may write programs that behave in a particular way only because that’s what their implementation happens to do. No amount of testing will reveal such a mistake, unless that testing uses a different implementation. Accordingly, programmers should avoid making such mistakes in the first place.

The rest of this article shows some particularly common examples of usages that might appear to work on some implementations, but are actually undefined. It also gives general rules that you can use to avoid such undefined behavior in your programs.

Multiple Side Effects

Consider the following loop, which copies one n-element array to another:

int i = 0;
while (i < n) {
   y[i] = x[i];
   ++i;
}

Looking at this loop, one might be tempted to shorten it as follows:

// Incorrect abbreviation
int i = 0;
while (i < n)
   y[i] = x[i++];

If this abbreviation works, it is only by coincidence. The problem is in the statement

y[i] = x[i++];

This statement both fetches and modifies the value of i within a single expression (more precisely, between two sequence points), and the language definition says that the results of such a statement are undefined.

We have seen people try to explain that the problem is that the compiler is permitted to evaluate the left- and right-hand sides of the assignment in either order, and therefore that i might be incremented before or after fetching y[i]. Although this explanation is logical and emotionally satisfying, and indeed it is a good explanation of the motivation for the language rule, it does not actually desctibe the rule. In fact, because of this statement’s multiple accesses to i, the C++ standard permits the implementation to do absolutely anything it pleases, including crash the program, produce no results at all, or anything else.

The implementation is under no obligation to diagnose such errors. Indeed, it may not even be possible for it to do so. Consider the following statement:

x[i] = x[j]++;

This statement is perfectly well defined — as long as i is not equal to j. If i and j are equal, then the statement is equivalent to

x[i] = x[i]++;

which accesses and changes the value of x[i] in a single expression. If i and j are unequal, on the other hand, then one element of x is accessed and the other modified, so there is no problem.

This example is typical of a problem that programmers often face: a piece of program that may look plausible on the surface is actually erroneous in a way that the compiler cannot, or does not, detect. Such problems are particularly important for programmers to detect, because they cannot count on the compiler doing so for them.

One way to avoid such problems is to know the rules. Another way, which is often easier, is to avoid writing programs about which you have any doubt. Yet another way, at least in this particular example, is to let the standard library do the work:

std::copy(x, x + n, y);

This rewrite shows an important way to simplify programs, and to make it less likely that they will run afoul of undefined behavior, namely to write them in terms of tools that others have already defined and tested.

Of course, you may object that using std::copy will be slower than writing the loop explicitly. However, there is no technical reason why there should be any difference in performance. Indeed, std::copy might even be faster on a good implementation. Because std::copy is part of the standard library, the implementation needs to do less work to figure out the intent of a call to std::copy than to figure out the intent of the corresponding loop, and therefore has an easier time generating optimal code.

Deleting Arrays

Another common example of undefined behavior often appears in programs that are intended to deal with variable-length character strings:

char* p = new char[n];
delete p; // should be delete[] p;

The language rule is simple: If you are allocating an array, you must use delete []; otherwise, you must not include the []. As with the previous example, it is not always possible for compilers to detect the error during compilation. In principle, it would be possible for them to detect it during execution, but most implementations would rather avoid the overhead of doing so.

What makes this example particularly insidious is that a number of implementations treat this example the same way: if you use new to allocate an array and delete without [] to free it, the implementation runs the destructor only on the first element of the array. This behavior is particularly convenient if the array elements are of a type, such as char, that does not have a destructor that does anything. Indeed, on such an implementation, this example will work just fine — which makes it hard for a programmer to determine that there is anything wrong with the program. For that matter, many programmers seem to believe — at least if we can judge by their Usenet postings — that this particular behavior is somehow required.

The C++ Standard offers no basis for such beliefs. If a program uses new to allocate an array, and uses delete without [] to deallocate it, the implementation is permitted to do whatever it pleases, including crash.

How can programmers avoid this disaster? The most obvious way is to check every use of delete against the corresponding use of new. However, as in the previous example, the most obvious solution is not always the best. The standard library provides classes, such as string and vector, that let many programs work with dynamic memory more easily than do the lower-level new and delete facilities. As before, the best way to avoid problems is to check first whether someone else has solved those problems for you.

Virtual Destructors

If you are allocating individual objects, rather than arrays, with new and delete, there is the possibility for another kind of undefined behavior. As an example, consider these classes:

class Base { };
class Derived: public Base { };

Suppose that we allocate an object of type Derived and put its address in a pointer to Base:

Base* bp = new Derived;

Then we can sometimes use that pointer to free the object:

delete bp;

Note first that it is correct not to use [] in this example, because we did not allocate an array. However, there is another question here. The pointer bp does not point to an object of the same type as was allocated. Is it safe to delete the object through that pointer?

The language definition is clear on this point: Such a delete is defined when Base has a virtual destructor, and undefined otherwise. Nevertheless, we have heard people claim that the delete is acceptable whenever class Derived has no members that require destruction. The rationale, which seems convincing even though it is incorrect, is that the implementation will use the type of the pointer, here Base, to determine what members to destroy; this determination is harmless when Derived has no extra members.

The most straightforward argument as to why this rationale is incorrect is that the language standard says otherwise — but such arguments are unsatisfying, because they do not explain why the standard says what it does.

We can see a more satisfying argument by noting what happens if we were to add a member to Derived that requires destruction:

// revised version
class Derived: public Base { string s; };

Now what should the implementation do when deleting bp? To require every implementation to check whether bp points to a Derived object would effectively impose the overhead of a virtual destructor on every class, even if the author knows that every delete always uses exactly the same type as the corresponding new. The whole point of having virtual destructors in the first place is to make it possible to reserve that overhead for contexts where it necessary. Therefore, implementations ought not to be required to deallocate the member s when deleting bp.

If implementations need not deallocate the member s, then deleting bp is potentially a memory leak when bp points to a Derived object. Accordingly, it is something that programmers should not do. Should implementations be required to check for that error? Again, it’s a matter of making overhead optional: if implementations were required to check that bp actually pointed to a Base object, they could just as well do the right thing when bp pointed to a Derived object. Therefore, requiring every implementation to check for the error would be just as slow, and less useful, than requiring every implementation to deallocate s.

One alternative would be to require every implementation not to check the type of object to which bp points. In that case, however, deleting bp when bp points to a Derived object would be an error that implementations would be prohibited from detecting, even if they wanted to do so.

Accordingly, the language definition leaves it up to the implementation what to do when a program uses a base-class pointer to delete a derived-class object when the base class has no virtual destructor. By leaving the behavior of such programs up to the implementation, the language definition allows implementations to check for the error if they wish to do so, and also allows implementations to minimize overhead by not checking if they prefer that approach. The way to avoid this problem in your programs is straightforward: unless there is an overwhelming reason to do otherwise, every class that is used as a base class should have a virtual destructor.

When Is It Safe to Use memcpy?

Our final example is a question that a colleague asked recently: When is it safe to use memcpy to copy elements from one array to another? For example, earlier we advocated rewriting

int i = 0;
while (i < n) {
   y[i] = x[i];
   ++i;
}

as

std::copy(x, x + n, y);

Couldn’t we have rewritten it as

memcpy(y, x, n * sizeof(x[0]));

instead? Surely, doing so would make our program run faster.

Like the previous three questions, this one has an answer: The language definition guarantees that memcpy will work in this example only if the elements of the arrays x and y are what is called a POD. The term POD stands for "plain old data;" loosely speaking, a POD is a type that has no user-defined copy constructor, assignment operator, or destructor, so that the bits that constitute its representation completely define its value. It is conceivable that memcpy will work for other types as well. For example, on many implementations, memcpy will copy classes such as string and vector perfectly well, provided that the programmer takes pains to avoid double destruction and memory leaks. For example:

string s, t;
// Swap the contents of s and t
// This code works only by accident
char x[sizeof(s)];
memcpy(x, (char*) &s, sizeof(s));
memcpy((char*) &s, (char*) &t, sizeof(s));
memcpy((char*) &t, x, sizeof(s));

This program fragment swaps the bits that constitute the representations of s and t. It will work on many implementations, because most implementations of the string class that we can imagine don’t care about the locations of the string objects themselves. Nevertheless, there is no requirement that any implementation permit this example to work, because string is not a POD type. We would be much better off exchanging the values of s and t this way:

std::swap(s, t);

Summary

Like C, C++ is a language that does not define the run-time effect of every program that the compiler will accept. Instead, programmers can do many things that the compiler will accept, and that will cause undefined behavior during execution.

Undefined behavior is truly that: the implementation is permitted to do anything at all. Because implementations have such wide latitude, they can even behave in ways that appear completely reasonable. Accordingly, it is not enough to assume that because your program appears to work, it actually works. It might be that the implementation, in behaving as it happens to behave, happens to behave the way you want. If you use a different implementation, or even a later release of the same implementation, or even if you run the program again under the same implementation, you might not be as lucky.

The surest way of avoiding undefined behavior is to be certain of what you know and what you don’t know. If you have the slightest question about the meaning of a program that you are about to write, write it differently. In each of these examples, there was an easy way to rewrite it so as to avoid all question. Opportunities for such rewriting are not hard to find, and they are well worth the effort.

Andrew Koenig is a member of the Large-Scale Programming Research Department at AT&T’s Shannon Laboratory, and the Project Editor of the C++ standards committee. A programmer for more than 30 years, 15 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++.

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 lectures worldwide.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.