Contents

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 toWriteExample
read something smallplain valueint, double, bool, char
read something bigconst&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

1. What does int& mean as a parameter type?

Yes. No copy is made, so assigning to the parameter assigns to the original. This is why the call site looks identical either way.

Not quite. Close in spirit and wrong in the details. Addresses are a real thing you can hold, and chapter 23 is where they arrive.

Not quite. It is not a copy at all. For an int a copy would in fact be the cheaper of the two.

2. A function only reads the std::vector<int> it is given. What should the parameter be?

Not quite. It does protect the original, by copying every element first. const& gives you the same protection without the copy.

Yes. The ampersand skips the copy, the const stops the function changing anything, and the compiler enforces the promise.

Not quite. It compiles and runs at the same speed, but nothing now stops the function modifying the caller’s data by accident.

3. Why is const int& a poor choice for a parameter?

Not quite. It works fine. const int is perfectly legal and chapter 3 used it.

Yes. A reference is something the machine follows to get at the value, which for a single int is more work than copying it.

Not quite. Reading is exactly what a const reference is for. What it stops is writing.

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: 22

Write 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.