Optimization: Calling By Value Or By Reference To Const?
Last week, I talked about architectural optimization, which I claimed must be taken into account as part of design. I would like to continue that discussion with another reason for thinking carefully about optimizations rather than making them automatically.
You are probably aware that if we define a function with a parameter that is not a reference, the corresponding argument will be copied when we call the function. For example:
class Thing {
// …
};
void foo(Thing t) { /* … */ }
Because the parameter to foo has type Thing, rather than type Thing& (with or without const), calling foo causes the C++ implementation to copy the object that we are passing to foo, thereby making the value of t inside foo distinct from the object that we gave foo as its argument.
Of course, without reading the definition of Thing, we don't know how long it takes to copy an object of that type. For that reason, many programmers are in the habit of defining functions such as Foo to take parameters of type const Thing& rather than of type Thing as an optimization.
I wish such optimizations were always so easy — but of course, if they were, the compiler would be doing it for us and we wouldn't need to worry about it. Alas, there are at least two problems that make it far from automatic to pass arguments to functions by reference. The following example illustrates both of these problems:
void scale(vector<double>& v, const double& x)
{
for (size_t i = 0; i != v.size(); ++i)
v[i] /= x;
}
This function divides every element in a vector<double> by a given value, x. The author of this function intended to avoid having to copy x each time through the loop by making x a reference to (const) double rather than a double directly. However, suppose we call the function this way:
scale(v, v[0]);
We want to divide each element of v by v[0]. Presumably we have ensured that v has at least one element and that v[0] is not equal to zero. If you look carefully at the code, however, you will see that it sets v[0] equal to v[0]/v[0] (i.e., to 1.0), and then divides every other element of v by 1.0, leaving those elements unchanged. By making x a reference, we have defined x as an alias to what, in this case, turns out to be an element of v itself.
Not only does this use of scale work incorrectly, but a compiler that compiles this code as we've written it will have to copy x each time through the loop anyway — even though we made x a reference to avoid just that possibility. The reason, of course, is that the compiler must cater to the possibility that x is an alias to an element of v, so it cannot store x in an internal register as it might otherwise have done.
If we were to define x as double rather than as const double&, we would be insisting that the program copy x at the beginning of the scale function. This copy would ensure that x would not be an alias for an element of v; so not only would the program now work as expected, but it would do so more quickly.
Defining a function parameter as a reference is a common optimization that avoids copying values that would otherwise have to be copied. However, like all optimizations, it should not be made automatically. Next week I shall discuss some other reasons that this optimization might not be as useful as it appears at first.

