Chapter 12
Copies, References, and Not Wasting Work
By value, by reference, and the const ampersand you will type for years.
Chapter 9 showed you this and moved on:
void add_ten(int n) {
n = n + 10;
}
int main() {
int score = 5;
add_ten(score);
std::cout << score << '\n'; // 5
}
The function got a copy. score never changed. That was the truth and it was
also, deliberately, only half of it.
One character changes everything
void add_ten(int& n) {
n = n + 10;
}
after copy: 5
after ref: 15
int& instead of int. Read it as “reference to int”, and read what it does
as: n is not a new variable this time, it is score, under a different name
for the duration of the call. Two names, one thing. Write to n and you have
written to score.
Nothing at the call site changes. It’s still add_ten(score);. This is worth
sitting with for a second, because it means you cannot tell from the call
whether a function will change your variable. You have to look at the function.
Some languages make you mark it at the call; C++ doesn’t.
That’s the argument for using & sparingly and naming such functions honestly.
add_ten(score) is fine because the name says it. A function called
print_report that quietly rearranges what you handed it is how afternoons
disappear.
Copying can be expensive
The other reason references exist has nothing to do with changing anything.
int total(std::vector<int> numbers) {
int sum = 0;
for (int n : numbers) {
sum += n;
}
return sum;
}
That’s chapter 10’s function, and it is fine for five numbers. Hand it a vector of
a million and C++ dutifully copies all million values into numbers before the
function starts, allocating the room and copying every element, so that the function
can read them and throw the copy away at the closing brace.
The reader can measure this rather than take my word for it. Timed here over 100 calls on a vector of a million ints, the copying version took around 480 ms and the version below took 285. Your numbers will differ; the shape won’t.
const and an ampersand
int total(const std::vector<int>& numbers) {
Two additions, doing two separate jobs.
The & says don’t copy: work with the caller’s vector directly. The const says
and don’t change it either. Together they mean let me look at your data without
touching it, which is what a function that reads something wants roughly always.
This is the same const from chapter 3, doing the same job in a new place. There
it meant a variable you promised not to reassign; here it means data you promised
not to modify. Both are notes to the compiler that it then enforces, and both fail
at compile time rather than at three in the morning. If chapter 3’s const felt
like paperwork, this is where it starts paying. A const& parameter is a promise
made once in the signature and checked on every line of the body.
Leave the const off and it still compiles and still runs fast, but you have
quietly given the function permission it doesn’t need. Put it on and the compiler
holds you to it:
void ruin(const std::vector<int>& numbers) {
numbers.push_back(99);
}
error: no matching member function for call to 'push_back'
note: candidate function not viable: 'this' argument has type
'const std::vector<int>', but method is not marked const
A reference is not only a parameter
Parameters are where you’ll use them most, but a reference is an ordinary thing you can declare anywhere:
std::vector<int> scores{10, 20, 30};
int& first = scores[0];
first = 99;
std::cout << "scores[0] is now " << scores[0] << '\n';
scores[0] is now 99
first is a second name for the vector’s first element. Handy when you’re about
to do several things to one deeply-buried value and don’t fancy typing
inventory[3] nine times.
Two rules come with it, and both are the compiler protecting you:
A reference must be given something to refer to, immediately.
int& r;
error: declaration of reference variable 'r' requires an initializer
There is no such thing as a reference to nothing. Chapter 23 introduces a different tool that can point at nothing, and this is the main reason to prefer a reference when either would do.
And it never changes its mind. Once first means scores[0], that’s
permanent:
int other = 5;
first = other;
scores[0] is now 5
other is still 5
That looks like it might have pointed first at other. It didn’t. first = other
means “put the value of other into whatever first refers to”, which is
scores[0]. A reference is welded on at birth. There is no syntax for moving it,
which is exactly why it’s the safe option.
When to use which
Three cases, and the boundary is less fussy than it looks:
| You want to | Write | Example |
|---|---|---|
| read something small | plain value | int, double, bool, char |
| read something big | const& | const std::string&, const std::vector<int>& |
| change the caller’s variable | & | void add_ten(int& n) |
“Small” means the handful of built-in types from chapter 3. Copying an int costs
nothing at all, and const int& for one is slower than the copy it avoids, because a
reference is a thing the machine has to follow.
Anything that can grow, meaning std::string, std::vector, and every container in the
rest of this book, goes by const& when you’re reading it. There is no size at
which you should start worrying about whether it’s big enough to bother; just
write it.
If you take one habit from this chapter: const& is the default for strings and
vectors you only read. You will type it for years.
The loop from chapter 10, improved
Range-for takes the same three options, for exactly the same reasons:
for (const std::string& word : words) {
std::cout << word << '\n';
}
Chapter 10 wrote that as for (std::string name : names), which copies every
string in turn. For three short names, nothing. For a vector of paragraphs, real
work for no reason.
And the third form does what you’d now expect:
for (int& n : numbers) {
n = n * 2;
}
That doubles the vector in place. No &, and it doubles a copy that is thrown
away a line later, and the vector is untouched. That is a bug that produces no error and
no output, and one you will now recognise on sight.
Exercise 1 · Feel the difference
Write void shout(std::string& text) that appends "!" to what it’s given.
Call it, print the string afterwards, and confirm it changed. Now take the &
off, run it again, and watch the change vanish.
Then write int letters(const std::string& text) that returns text.size().
Try to add text += "!"; inside it and read the error the compiler gives you,
first two lines only.
Finally, take a std::vector<int> of five numbers and double every one with a
range-for. Get it wrong on purpose first, without the &, so you have seen what
the silent version looks like.
Check yourself
Project
Text statistics
Roughly 45 minutes
One list of words, four functions that report on it. Nothing here is new except the parameter types, and that is the point. This is the first program where the signatures are the design.
int count_words(const std::vector<std::string>& words) {
return words.size();
}
std::string longest_word(const std::vector<std::string>& words) {
std::string best = "";
for (const std::string& word : words) {
if (word.size() > best.size()) {
best = word;
}
}
return best;
}
int total_letters(const std::vector<std::string>& words) {
int total = 0;
for (const std::string& word : words) {
total += word.size();
}
return total;
}words: 5
longest: jumped
letters: 22Write a main that builds a vector of words and prints all three. Then add a
fourth of your own: shortest_word, or count_starting_with, which needs a
second parameter.
Every one of these takes const std::vector<std::string>& and returns a fresh
value. Read the three signatures together: without looking at a single line of
the bodies, you know none of them changes your list. That is what the type is
for.
Then break the pattern deliberately. Add:
void add_word(std::vector<std::string>& words,
const std::string& word);One parameter by reference because it gets changed, one by const& because it
only gets read, in the same set of brackets. Once that reads naturally to you,
this chapter has done its job.
Stretch: remove the empties. Write
void drop_empty(std::vector<std::string>& words) that removes any empty
strings. Building a second vector and assigning it over the first is the
straightforward way, and there is a sharper way that chapter 29 gets to.