References and Parameter Passing
Module 6 gave you a rule: a function works on a copy. Changing a parameter does not change the caller's variable. That rule is safe, and it has two costs.
- A function cannot change a variable of its caller, even when that is its whole job.
- Every call copies its arguments. Copying one
intcosts nothing worth counting. Copying a vector of ten thousand strings, only to count them, is wasted work.
Both costs have the same cure. A reference is a second name for a variable that already exists. This lesson shows what a reference is, and then uses it in parameters and in loops.
Another name for the same variable
This program has three names and two variables.
#include <iostream>
int main() {
int stock = 12;
int snapshot = stock;
int& shelf = stock;
snapshot = 3;
shelf = 20;
std::cout << "stock: " << stock << "\n";
std::cout << "snapshot: " << snapshot << "\n";
std::cout << "shelf: " << shelf << "\n";
}
stock: 20
snapshot: 3
shelf: 20
int snapshot = stock; is a declaration of the kind you know. It makes a new variable and copies the value 12 into it.
int& shelf = stock; is new. The & after the type makes shelf a reference: no new variable is made, and no value is copied. shelf becomes another name for the variable stock. Read int& as "reference to an int".
| Statement | stock |
snapshot |
shelf |
|---|---|---|---|
int stock = 12; |
12 | ||
int snapshot = stock; |
12 | 12 | |
int& shelf = stock; |
12 | 12 | 12, the same variable as stock |
snapshot = 3; |
12 | 3 | 12 |
shelf = 20; |
20 | 3 | 20 |
snapshot = 3; changes the copy, and stock does not follow. shelf = 20; stores 20 in the one variable that has two names, so stock reads 20 as well. It works in both directions: after stock = 1; the name shelf reads 1.
Two rules come with a reference.
- A reference must be given its variable where it is declared.
int& shelf;on its own does not compile, because a name must be a name for something. - A reference names the same variable for as long as it exists. An assignment to it never moves it to another variable.
shelf = snapshot;means "store the value ofsnapshotinstock".
A reference inside one function, as here, is of little use: you could write stock instead. References earn their place when the two names are in two different functions.
A parameter that is the caller's variable
Here is double_it from module 6 with one character added: the & in its first line.
#include <iostream>
void double_it(int& n) {
n = n * 2;
}
int main() {
int apples = 4;
double_it(apples);
std::cout << "apples in main: " << apples << "\n";
}
apples in main: 8
In module 6 this program printed 4. The parameter was int n, a new variable with a copy of the value. Now the parameter is int& n, a reference, and a reference parameter gets its variable from the call. During the call double_it(apples); the name n is another name for main's variable apples.
| Moment | apples (in main) |
n (in double_it) |
|---|---|---|
| before the call | 4 | does not exist |
| the call starts | 4 | another name for apples |
after n = n * 2; |
8 | 8, because it is apples |
| the call has ended | 8 | does not exist |
Nothing was copied in and nothing was copied back. There was one variable all along. This is called pass by reference: the function is passed the variable, not its value.
Each call binds the parameter again. In double_it(pears); the name n would mean pears.
Three things to notice.
- The call looks the same in both versions:
double_it(apples);. Only the first line of the function says whether the caller's variable can change. When you read a call to a function you do not know, look at its first line. - The argument must be a variable.
double_it(4);does not compile, and neither doesdouble_it(apples + 1);. There would be no variable fornto name, and nowhere to store the result. - The type must match exactly. An
int¶meter cannot name adoublevariable.
Strings and vectors are passed by reference in the same way. A vector parameter written without & has been a copy of the whole vector since module 9. With & it is the caller's vector.
#include <iostream>
#include <vector>
void add_twice(std::vector<int>& list, int value) {
list.push_back(value);
list.push_back(value);
}
void remove_all(std::vector<int>& list) {
std::vector<int> nothing;
list = nothing;
}
int main() {
std::vector<int> laps = {61, 58};
add_twice(laps, 60);
std::cout << laps.size() << "\n";
remove_all(laps);
std::cout << laps.size() << "\n";
}
4
0
add_twice has one parameter of each kind. list is the caller's vector, and the two new elements are added to laps itself. value is an ordinary copy of 60.
remove_all shows that a whole vector can be assigned, like an int. After list = nothing; the caller's vector holds a copy of the elements of nothing, and there are none.
Looking without copying
A function that only reads a vector has no need to change it. Passed by value, the vector is still copied on every call, element by element. Passed by reference, it is not copied, but then the function is able to change it, and a reader of the call can no longer be sure that it does not.
A const reference gives the first advantage without the second risk. You know const from module 2: a const int is a variable the compiler will not let you change. const in front of a reference means: this name may be used to read the variable, and never to change it.
#include <iostream>
#include <string>
#include <vector>
int total_of(const std::vector<int>& numbers) {
int total = 0;
for (int n : numbers) {
total += n;
}
return total;
}
void print_label(const std::string& label) {
std::cout << "[" << label << "] has " << label.size() << " characters\n";
}
int main() {
std::vector<int> weights = {12, 30, 7};
std::cout << total_of(weights) << "\n";
std::string name = "front door";
print_label(name);
print_label("gate");
}
49
[front door] has 10 characters
[gate] has 4 characters
During the call total_of(weights), numbers is another name for weights. No element is copied. Everything that only reads is allowed: numbers.size(), numbers[0], a loop over the elements.
Anything that would change the vector is refused by the compiler. This version tries to tidy up after itself, and does not compile.
#include <iostream>
#include <vector>
int total_of(const std::vector<int>& numbers) {
int total = 0;
for (int n : numbers) {
total += n;
}
numbers.push_back(total);
return total;
}
int main() {
std::vector<int> weights = {12, 30, 7};
std::cout << total_of(weights) << "\n";
}
So the first line int total_of(const std::vector<int>& numbers) is a promise that the compiler enforces. Whoever calls it knows, without reading the body, that their vector comes back as it was.
The promise is about the name numbers, not about the variable. weights is not const, and main may go on changing it.
There is one more difference from a plain reference. The call print_label("gate"); gives a value, not a variable, and it compiles. A function that cannot change its argument has no need for a variable to store anything in, so a const reference accepts a value as well. A plain std::string& parameter would refuse "gate".
Use a const reference for every string or vector parameter that the function only reads. That is the usual way to pass them. For an int, a double, a bool or a char, keep passing by value: the copy is as cheap as the reference.
Changing the elements of a vector
The loop for (int x : v) from module 9 gives each element to x as a copy. The declaration before the colon follows the rules you have now seen three times: int x is a new variable, int& x is another name.
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<int> marks = {41, 67, 58};
for (int m : marks) {
m = m + 5;
}
std::cout << marks[0] << " " << marks[1] << " " << marks[2] << "\n";
for (int& m : marks) {
m = m + 5;
}
std::cout << marks[0] << " " << marks[1] << " " << marks[2] << "\n";
std::vector<std::string> towns = {"Ely", "Wells", "Ripon"};
int letters = 0;
for (const std::string& town : towns) {
letters += town.size();
}
std::cout << letters << "\n";
}
41 67 58
46 72 63
13
The first loop adds 5 to m three times, and m is a copy each time. The vector is unchanged, as the first line of output shows. The compiler does not complain: changing a copy is allowed, only useless.
In the second loop m is, on each turn, another name for the next element. m = m + 5; changes the element itself.
The third loop only reads. const std::string& town names each string in turn without copying it, and the compiler would refuse town = "York";. With plain std::string town, each of the three strings would be copied once to be measured.
auto works in all three forms. for (auto& m : marks) and for (const auto& town : towns) mean the same as the loops above: auto stands for the element type, and you add the & or the const and & yourself. auto on its own never makes a reference: for (auto m : marks) copies.
The choice is the same as for a parameter.
| Loop | Each turn, the name is | Use it to |
|---|---|---|
for (int x : v) |
a copy of the element | read small elements |
for (int& x : v) |
the element | change the elements |
for (const std::string& s : v) |
the element, read only | read strings or other large elements |
Exchanging two variables
A common small job is to exchange the values of two variables: afterwards each holds what the other held. The obvious two assignments do not do it.
#include <iostream>
int main() {
int left = 3;
int right = 8;
left = right;
right = left;
std::cout << left << " " << right << "\n";
}
8 8
left = right; stores 8 in left, and the 3 is gone. Nothing holds it any more, so the second assignment can only copy the 8 back. A variable holds one value, and storing a new one destroys the old.
The cure is a third variable that keeps one value safe while its variable is overwritten.
Written as a function, the exchange must reach the caller's two variables. With parameters passed by value it would exchange two copies and the caller would see nothing. So both parameters are references.
#include <iostream>
void exchange(int& a, int& b) {
int kept = a;
a = b;
b = kept;
}
int main() {
int left = 3;
int right = 8;
exchange(left, right);
std::cout << left << " " << right << "\n";
int low = 40;
int high = 15;
if (low > high) {
exchange(low, high);
}
std::cout << low << " " << high << "\n";
}
8 3
15 40
Follow the first call, where a is left and b is right.
| Statement | left (a) |
right (b) |
kept |
|---|---|---|---|
| the call starts | 3 | 8 | |
int kept = a; |
3 | 8 | 3 |
a = b; |
8 | 8 | 3 |
b = kept; |
8 | 3 | 3 |
kept is an ordinary int, a copy. That is what is wanted: it must go on holding 3 after a has changed. Had it been declared int& kept = a; it would be a third name for left, and it would read 8 on the last line.
The second half of main shows what an exchange is most often used for: putting two values in order. Exchange them if they are the wrong way round, and afterwards low is not greater than high.
More than one result
return gives back one value. Some jobs produce two. A number of minutes, for example, is a number of hours and a number of minutes left over.
A function can deliver several results by storing them in variables of the caller, through reference parameters. Such a parameter is called an output parameter: the function does not read it, it fills it.
#include <iostream>
#include <vector>
void split_minutes(int total, int& hours, int& minutes) {
hours = total / 60;
minutes = total % 60;
}
bool first_negative(const std::vector<int>& numbers, int& position) {
int i = 0;
for (int n : numbers) {
if (n < 0) {
position = i;
return true;
}
++i;
}
return false;
}
int main() {
int h = 0;
int m = 0;
split_minutes(135, h, m);
std::cout << h << " h " << m << " min\n";
std::vector<int> changes = {4, 0, -6, 9, -1};
int where = 0;
if (first_negative(changes, where)) {
std::cout << "first negative at position " << where << "\n";
}
}
2 h 15 min
first negative at position 2
split_minutes has one input, total, passed by value, and two outputs. The caller declares h and m first, then hands them over to be filled.
first_negative shows a common pattern. Its return value answers "did it work?", and the output parameter carries the answer only when it did. If no element is negative the function returns false and leaves position alone, so the caller must test the returned bool before using where.
Output parameters work, and they make a call harder to read than a return value does.
- In
split_minutes(135, h, m);nothing shows which arguments go in and which come out. Withint area = square(side);the direction is plain. - The caller needs variables declared beforehand, with starting values that are never used.
- The call cannot be used inside an expression, as
square(3) + square(4)can.
So when a function has one result, return it. Keep output parameters for a function with two or more results, or for the pattern "a bool for success, and a result". Module 13 shows another way to return several values at once.
Which kind of parameter?
You now have three ways to write a parameter. This function uses all three.
#include <iostream>
#include <string>
#include <vector>
void add_copies(std::vector<std::string>& list, const std::string& item, int copies) {
for (int i = 0; i < copies; ++i) {
list.push_back(item);
}
}
int main() {
std::vector<std::string> basket;
std::string drink = "tea";
add_copies(basket, drink, 2);
add_copies(basket, "rice", 1);
add_copies(basket, "salt", 0);
for (const std::string& item : basket) {
std::cout << item << "\n";
}
}
tea
tea
rice
The first line of add_copies can be read as a contract.
std::vector<std::string>& listsays: I may change your vector.const std::string& itemsays: I will look at your string, without copying it and without changing it.int limitsays: I take a copy of a small value. Whatever I do with it is my own business.
To choose for a parameter of your own, ask two questions in this order.
- Must the function change the caller's variable? Then a reference:
T&. - If not: is the value a string, a vector, or something else that is costly to copy? Then a const reference:
const T&. Otherwise pass by value:T.
| Written | The function gets | Can it change the caller's variable? | The argument may be |
|---|---|---|---|
int n |
a copy | no | any value |
int& n |
the caller's variable | yes | a variable only |
const std::string& s |
the caller's variable, read only | no | any value |
Two reminders.
- By value is still the default for small types, and it is still a guarantee. After
f(count);where the parameter is anint,countis unchanged. - A plain reference is the unusual one. Use it only when changing the caller's variable is what the function is for, and give the function a name that says so:
add_copies,exchange,remove_all.
Returning a vector, as in module 9, has not changed. A function that builds something new returns it. A function that alters something that exists takes it by reference.