The fact that C++ allows operators to be overloaded for user-defined
classes can make programming with library classes like Integer
,
String
, and so on very convenient. However, it is worth
becoming familiar with some of the inherent limitations and problems
associated with such operators.
Many operators are constructive, i.e., create a new object based on some function of some arguments. Sometimes the creation of such objects is wasteful. Most library classes supporting expressions contain facilities that help you avoid such waste.
For example, for Integer a, b, c; ...; c = a + b + a;
, the
plus operator is called to sum a and b, creating a new temporary object
as its result. This temporary is then added with a, creating another
temporary, which is finally copied into c, and the temporaries are then
deleted. In other words, this code might have an effect similar to
Integer a, b, c; ...; Integer t1(a); t1 += b; Integer t2(t1);
t2 += a; c = t2;
.
For small objects, simple operators, and/or non-time/space critical programs, creation of temporaries is not a big problem. However, often, when fine-tuning a program, it may be a good idea to rewrite such code in a less pleasant, but more efficient manner.
For builtin types like ints, and floats, C and C++ compilers already
know how to optimize such expressions to reduce the need for
temporaries. Unfortunately, this is not true for C++ user defined
types, for the simple (but very annoying, in this context) reason that
nothing at all is guaranteed about the semantics of overloaded operators
and their interrelations. For example, if the above expression just
involved ints, not Integers, a compiler might internally convert the
statement into something like c = a; c += b; c+= a;
, or
perhaps something even more clever. But since C++ does not know that
Integer operator += has any relation to Integer operator +, A C++
compiler cannot do this kind of expression optimization itself.
In many cases, you can avoid construction of temporaries simply by
using the assignment versions of operators whenever possible, since
these versions create no temporaries. However, for maximum flexibility,
most classes provide a set of "embedded assembly code" procedures
that you can use to fully control time, space, and evaluation strategies.
Most of these procedures are "three-address" procedures that take
two const
source arguments, and a destination argument. The
procedures perform the appropriate actions, placing the results in
the destination (which is may involve overwriting old contents). These
procedures are designed to be fast and robust. In particular, aliasing
is always handled correctly, so that, for example
add(x, x, x);
is perfectly OK. (The names of these procedures
are listed along with the classes.)
For example, suppose you had an Integer expression
a = (b - a) * -(d / c);
This would be compiled as if it were
Integer t1=b-a; Integer t2=d/c; Integer t3=-t2; Integer t4=t1*t3; a=t4;
But, with some manual cleverness, you might yourself some up with
sub(a, b, a); mul(a, d, a); div(a, c, a);
A related phenomenon occurs when creating your own constructive
functions returning instances of such types. Suppose you wanted
to write function
Integer f(const Integer& a) { Integer r = a; r += a; return r; }
This function, when called (as in a = f(a);
) demonstrates a
similar kind of wasted copy. The returned value r must be copied
out of the function before it can be used by the caller. In GNU
C++, there is an alternative via the use of named return values.
Named return values allow you to manipulate the returned object
directly, rather than requiring you to create a local inside
a function and then copy it out as the returned value. In this
example, this can be done via
Integer f(const Integer& a) return r(a) { r += a; return; }
A final guideline: The overloaded operators are very convenient, and much clearer to use than procedural code. It is almost always a good idea to make it right, then make it fast, by translating expression code into procedural code after it is known to be correct.