Why Would You Ever Pass a Container By Value?
An important part of teaching is figuring out how to avoid drowning students in detail. Most people who have tried to learn something new and complicated, such as a programming language, have had the experience of looking at a textbook, or listening to a lecture, and encountering a flood of options or features without a clue as to which ones are important, or which ones to use when, or even how they relate to each other. One way to avoid imposing such experiences on students is to give them an intellectual framework that lets the students relate parts of what they are learning to other parts.
For example, suppose we write a C++ expression that calls a function:
f(x);
Obviously, this expression calls a function f
and passes an argument x
to it. Moreover, if f
is defined as
void f(T y) { /* … */ }
then we can say that the call f(x)
passes the argument x
to the parameter y
, and that doing so involves the same actions as if we had written
T y = x;
This explanation encourages students to relate what happens when we call a function to what happens when we use a value to initialize a variable. However, although this relation may simplify learning, it complicates teaching. Consider two fundamental features of C++: functions and references. Which shall we teach first?
If we teach references first, there is the problem of coming up with interesting example programs that use references but completely avoid user-defined functions. This is hard to do because the most common use of references is as function parameters — so it's probably easier to teach functions first.
However, if we teach functions before we teach references, then every function we write must accept its arguments by value — references not yet being available as an alternative. That is, every function call must copy the function's arguments to its parameters. There is no problem justifying such behavior if those arguments are small values, such as integers; but if our first example of passing a container to a function copies the entire container, we are encouraging students to write code that does needless work.
In Accelerated C++, Barbara and I came up with the idea of having the first such function compute the median of its argument. The most straightforward way to compute the median of a container is to sort the container — which changes its original value — and then to locate the element or elements in the middle. As a result, we were able to finesse the pedagogical problem of passing a container to a function by having that function compute the median, changing the value of its parameter in the process.
The picture changes when we expand the picture to include C++11 move operations. Now, when we write
T y = x;
the effect is to copy or move x
to y
depending on whether x
is an lvalue. Similarly, when we call f(x)
, the effect is to copy or move x
to f
's parameter y
depending on whether x
is an lvalue. This dependency not only changes how we explain what is going on, but also changes the circumstances under which such functions do needless work.
In the presence of move
operations, I think it might be better to explain functions with string
parameters before we explain functions with vector
parameters. Not only are string
s probably more common than vector
s, but string
rvalues are surely more common than vector
rvalues. For example:
bool is_palindrome(string s) { /* Details left to the reader */ }
Suppose we call is_palindrome("radar")
. Then what really happens under the hood is that the string literal "radar"
has type const char*
, which is different from the type of is_palindrome
's parameter. Accordingly, "radar"
is used to construct a temporary of type string
. That temporary is an rvalue, so it is moved, not copied, to the parameter s
of is_palindrome
. An analogous phenomenon happens when we pass the result of a nontrivial expression to such a function. As a result, we have a fairly wide range of examples from which to choose without having to write code that does needless work. Moreover, we have plenty of opportunities to return to these examples for more detailed study later.
In short, the question posed in this article’s title has at least two answers:
- When you know that the function is going to change the value of its parameter; or
- When you're confident that most of the time, you'll be passing a container rvalue.
I'll continue this discussion next week by showing why the "needless effort" in some of these functions is worth making a fuss to avoid. In particular, I'd like to explain the apparent contradiction between wanting to avoid needless effort and my long-held belief that computers should be working for people, not the other way around.