Columns


Stepping Up To C++

Temporary Inconvenience, Part 1

Dan Saks


Dan Saks is the founder and principal of Saks & Associates, which offers consulting and training in C++ and C. He is secretary of the ANSI and ISO C++ committees. Dan is coauthor of C++ Programming Guidelines, and codeveloper of the Plum Hall Validation Suite for C++ (both with Thomas Plum). You can reach him at 393 Leander Dr., Springfield OH, 45504-4906, by phone at (513)324-3601, or electronically at dsaks@wittenberg.edu.

Bjarne Stroustrup has described C++ as "an engineering compromise." In designing the language, he tried to strike a difficult balance. He extended C at the high end with support for data abstraction and object-oriented programming. At the same time, he did not want to sacrifice the efficiency and machine-level capabilities that have made C so successful.

As with any good compromise, few are delighted by the result, and many are disappointed. Object-oriented purists see C++ as a language that encumbers programmers with too many details of resource management. Conservative C programmers eye the same object-oriented features suspiciously, concerned that they carry hidden costs.

This spirit of compromise is very evident in C++'s approach toward temporary objects that hold intermediate results during expression evaluation. Ideally, C++ programmers shouldn't have to concern themselves with exactly when the temporaries come and go. But they must. Under the worst circumstances, a program might inadvertently destroy a temporary before it's done with it. Or, almost as bad, it might tie up resources by keeping temporaries around long after they've ceased being useful.

A compiler must insure that each program destroys every temporary it creates. However, the Annotated C++ Reference Manual (ARM) (Ellis and Stroustrup [1990]) grants each implementation complete freedom to destroy temporaries when it sees fit. For a few years now, the C++ standards committee has generally agreed that the emerging standard (based on the ARM) leaves programmers with too little guidance for writing portable programs. But only recently could we agree on what to do about it.

This month I'll explain why C++ programs create temporary objects. Next month I'll explain how variations in the lifetime of temporary objects cause problems, and how the new rules adopted by the standards committee eliminate many of those problems.

How Temporaries Happen

A C++ compiler may create temporary objects to hold the intermediate results of an expression. For example, given objects

T w, x, y, z;
of some type T, a compiler may translate an expression such as

w = x + y + z;
into something like

T temp1 = x + y;
T temp2 = temp1 + z;
w = temp2;
When T is a primitive type, like int or double, an alert compiler can eliminate the temporaries by simply accumulating the result in w. In that case, the generated code would be equivalent to

w = x;
w += y;
w += z;
But when T is a class type, compilers must be more cautious about eliminating temporaries for fear of altering the semantics of the expression.

Overloaded operators don't necessarily preserve relationships that hold when applying those operators to predefined types. For example, when you overload operators + and += for a class T, you should define the operators so that given

T x, y;
the expressions

x = x + y;
and

x += y;
produce the same results. (Plum and Saks [1991] suggest preserving this and other intuitive relationships between operators when you overload them.) But this is purely a matter of style and a C++ compiler can't assume that the relationship holds.

Common Uses for Temporaries

Unnamed temporary objects are most useful in evaluating two kinds of subexpressions:

Functions Returning Objects

Listing 1 shows an implementation of an operator+ that adds two rational numbers. A rational number is an exact fraction represented by two (signed) long integers. For example,

rational r(3, 4);
defines r as a rational number whose value is (conceptually) 3/4. (I implemented this class in considerable detail in a four-part series starting with "Operator Overloading", CUJ, Nov. 1991.)

The operator+ in Listing 1 is only one of many possible implementations. As I described in the series on operator overloading, you can implement a binary operator like + as a member, a non-member friend, or as an (unfriendly?) non-member. Furthermore, you can pass the arguments to operator+ by value (as in Listing 1) , or as references to constant objects (ref-to-const) as in

rational operator+
   (const rational &r1, const rational &r2);
However, no matter how you declare the function, it should return a rational by value. That is, the return type should be rational, not rational * or rational &.

Let's look more carefully at the possible parameter passing and function return conventions. rational+ takes two rational operands and returns a third. You should not return the result in one of its operands, as in

rational &operator+(rational &r1, rational &r2)
   {      // BAD IDEA
   r1.num = r1.num * r2.denom + r2.num * r1.denom;
   r1.denom *= r2.denom;
   return r1;
   }
Otherwise you will find that

rational r, s, t;
// ...
r = s + t;
has the surprising side-effect of changing the value of s. Thus you should pass both arguments either by value or by ref-to-const.

If you pass the arguments by value, you can use the formal parameters as local variables and compute the result in one of them, as in

rational operator+(rational r1, rational r2)
   {
   r1.num = r1.num * r2.denom + r2.num * r1.denom;
   r1.denom *= r2.denom;
   return r1;
   }
In this case, you must pass the result back through the function return value. Normally, you want that return type to be an object rather than a pointer or reference to an object. If it were a pointer or reference, what would it be bound to? You can't bind it to a local variable or one of the formal parameters, as in

rational &operator+(rational r1, rational r2)
   {
   r1.num = ...
   r1.denom = ...
   return r1;
   }
because returning from the function destroys its local variables and formal parameters. Returning a reference to a local object produces a dangling reference.

If you really want to return a reference, you could bind the reference to a local static object, as in

rational &operator+(rational r1, rational r2)
   {    //ALSO NOT WISE
   static rational result;
   result.num = ...
   result.denom = ...
   return result;
   }
But, if you did this with all the rational binary operators, you might have trouble evaluating complicated expressions like

r= s * t + u * v;
Here, the subexpressions s * t and u * v store their results in the same place, and one might overwrite the result of the other before operator+ has fetched the result of the first.

Yet another possibility is

rational *operator+(rational r1, rational r2)
   {    // ALSO NOT WISE
   rational *result = new rational;
   result->num = ...
   result->denom = ...
   return result;
   }
where the function returns a pointer to a new rational object allocated from the free store. But this function is awkward to use because you must dereference the pointer returned from the function, as in

r = *(s + t);
Furthermore, calling operator+ like this causes a memory leak. The assignment copies the value referenced by the pointer, and then loses the pointer, leaving the program without a way to delete the object referenced by the pointer.

There are still other ways to return a value, but they all reinforce the same conclusion: an overloaded binary operator should normally return its result by value.

The program may return that value in an unnamed temporary object. For example, consider the following:

rational operator+(rational r1,
                rational r2)
   {
   rational result;
   // ...
   return result;
   }
As it returns, this function copies the result into a temporary rational object using the rational copy constructor. An expression like

r = s + t;
translates into code that returns the result of s + t in a temporary and passes that temporary as the right-hand operand of operator=. That is, it produces code something like

rational temp1(operator+(s, t));
r.operator= (temp1);
destroy temp1;
The commentary at the end of chapter 12 in the ARM suggests alternative code generation techniques that reduce the need for temporaries. For example, a C++ compiler might translate a function returning a value, like

rational operator+(rational r1,
                rational r2);
into a function with three pointer arguments and a void return type:

void rational_add(rational *result,
               rational *arg1,
               rational *arg2);
Using this approach, a declaration like

rational r3 = r1 + r2;
translates into

rational_add(&r3, &r1, &r2);
which eliminates the need for a temporary. However, the ARM goes on to show that even when using this translation technique, the compiler may have to generate temporaries to avoid aliasing errors (when the result object is also one of the operands). For example, an expression like

r1 = r2 + r1
translates into

rational_add(&r1, &r2, &r1);
But there's a danger that rational_add may store a partial result in r1 before it's done using r1's initial value. Thus, a conscientious compiler that uses this translation scheme must translate

r1 = r2 + r1;
into something like

rational temp1;
rational_add(&temp1, &r2, &r1);
r1 = temp1;
C++ translators must retain the right to introduce temporary objects as needed.

Constructor Calls

Although unnamed temporaries seem to occur most often in expressions involving overloaded operators, they can just as easily occur from ordinary function calls. For example, consider the call f(1).g() inside function h in Listing 2. f is a non-member function that returns an object of class X, and g is a member of X. Thus, f(1).g() applies X::g to the X object returned from f in an unnamed temporary.

Temporary objects also result from calls to constructors inside expressions. For instance, function h in Listing 2 also contains

j = X(3).g();
X(3) invokes the constructor X(int) producing a temporary X object. X(3).g() applies X::g to that temporary object.

Temporaries Bound to References

A C++ program may also create temporary objects when initializing references. As I explained in an earlier column ("Stepping Up to C++: Reference Types," CUJ, September, 1991), the initializer for a reference to type T should be an lvalue of type T. For example,

char c;
char &rc = c;
binds rc (of type reference to char) to the char object c.

A reference can refer to a const object. In this case, if the initializing expression is not an lvalue, the generated code creates a temporary object to hold the initial value, and binds the reference to that temporary. For example,

const double &rcd = 1.0;
compiles as something like

const double temp = 1.0;
const double &rcd = temp;
The initializing expression for a const T & need not have type T; it can be an expression of any type with a standard or user-defined conversion to type T. For example,

const double &rcd = 1;
promotes 1 to double to initialize the temporary. Or, if X is a class with a constructor X::X(int), then

const X &rcx = 1;
is more-or-less equivalent to

const X temp(1);
const X &rcx = temp;
Early dialects of C++ (Stroustrup [1986]) created temporaries even for non-const references. That is, C++ used to accept

double &rd = 1;
and bound the reference to a temporary initialized with 1. Subsequently, an assignment like

rd = 2;
changed the value of the temporary to 2.

The commentary in the ARM explains that binding references to modifiable temporaries proved to be "a major source of errors and surprises." But the example I gave above isn't so surprising. The real surprises came from examples like the one in Listing 3.

Listing 3 shows a function f that returns a value through its reference parameter double &r. The call f(n) inside g looks like it should return a value in n. But n is an int, not a double. Under the old C++ rules the call f(n) generates a temporary double object initialized with n and binds reference parameter r to the temporary. The assignment inside f stores the result into the temporary, not into n. Surprise!

The more recent language definition in the ARM prohibits creating temporaries to initialize references to non-const objects. Many of today's C++ implementations still follow the older, more forgiving rule, but issue warnings when they create temporaries bound to non-const references.

Lifetimes of Temporaries

The lifetime of a temporary object is the period of time during program execution from the creation of that temporary until its destruction. The ARM, and until recently, the Working Paper for the C++ standard, left the lifetime of temporary objects "implementation dependent." A program might destroy each temporary almost immediately, or it might save them up and destroy them all at program termination.

Next month, I'll look at specific problems resulting from the uncertainty of the lifetime of temporaries, and I'll describe the new rules that eliminate many of those problems.

The Range of an Enumeration

I received an interesting letter last month:

In "Recent Language Extensions to C++," by Dan Saks (CUJ, June 1993), the definition of the operator++ in the section on operator overloading for enumerations is flawed. As described, it would give an undefined result for the last color BLUE. If the semantics of incrementing BLUE meant going back to RED, then the following could work:

enum color { RED, WHITE, BLUE };
color &operator++(color &c)
   {
   switch (c)
      {
      case RED: return c = WHITE;
      case WHITE: return c = BLUE;
      case BLUE: return c = RED;
      }
   }
Thus is of course ugly for larger enums. Maybe your readers can suggest a more elegant solution.

Rizwan Mithani
Stoner Associates
P.O. Box 86
Carlisle, PA 17013-0086

Let me say this about that. In the article in question I explained that, given

enum color { RED, WHITE, BLUE };
enum color c;
a for loop like

for (c = RED; c <= BLUE; ++c)
no longer compiles as C++. It compiles as C because C considers enumerations to be integral types and lets you apply ++ to an enumeration. But a recent change in C++ removed enumerations from the integral types so that ++ no longer applies.

Overloading for enumerations provides a way to restore the for loop above as valid C++. I suggested you could define ++ for colors as:

inline color &operator++(color &c)
   {
   return c = color(c + 1);
   }
My intent was not, as the letter suggests, to write ++ so that incrementing a color whose value is BLUE makes it wrap around to RED. That causes the loop to repeat without end. I intended for the terminating value for c in

for (c = RED; c <= BLUE; ++c)
to be the color whose value is BLUE+1. I believe this has well-defined behavior in C++.

The C++ standards committee realized that restricting the values of an enumeration variable to only the enumerators for that type was too restrictive given widespread practice in C and C++. For example, C and C++ programmers write things like

enum mode { in = 1, out = 2 };
int open(const char *s, mode m);

void f()
   {
   // ...
   open(fn, in | out);
   // ...
The expression in | out yields a value, namely 3, that is not one of the enumerators. The committee decided that an enumeration object could legitimately have values other than its named enumeration values. The new rules for enumeration are essentially:

For example, given

enum color { RED, WHITE, BLUE };
e-min is 0 (RED), and e-max is 2 (BLUE). I believe the underlying type for color is unsigned char. (I realize as I write this that the proposed new wording is not as clear as I'd like it to be for determining the underlying type.) In any case, the range of values for color is at least the set of values that can be represented in a bitfield that can hold 0 through 2, inclusive. This means that the underlying value 3 (BLUE+1) is also a legitimate value for a color, because it fits in that bitfield.

Had I defined color as

enum color { RED, WHITE, BLACK, BLUE };
then e-max would be 3 (BLUE), and the range from e-min to e-max would completely fill the bitfield. Then BLUE+1 would be outside the range of enumeration values and the ++operator might not work as I intended. This suggests that, as a general style rule, if you want to be able to increment through the entire range of values for an enumerator, you should insure that the value one greater than the largest enumerator is in the range of values. In other words, the largest enumerator shouldn't have the value 2n-1 (for some positive integer n). If necessary, add an extra enumerator at the end just to increase the range.

Finally, if you do want operator++ to cause the enumeration values to wrap around from the largest to the smallest enumerator, you need not use a case statement as suggested above. Rather, try writing

color &operator++(color &c)
   {
   if (c == LAST_COLOR)
      c = FIRST_COLOR;
   then
      c = color (c+1);
   return c;
   }
where FIRST_COLOR and LAST_COLOR are the smallest and largest color values, respectively.

References

Ellis and Stroustrup [1990], Margaret A. Ellis and Bjarne Stroustrup, The Annotated C++ Reference Manual, Addison-Wesley.

Plum and Saks [1991], Thomas Plum and Dan Saks, C++ Programming Guidelines, Plum Hall.

Stroustrup [1986], Bjarne Stroustrup, The C++ Programming Language (1st ed.), Addison-Wesley.