Build with C++

References and Parameter Passing

On this page

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 int costs 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 of snapshot in stock".

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 does double_it(apples + 1);. There would be no variable for n to name, and nowhere to store the result.
  • The type must match exactly. An int& parameter cannot name a double variable.

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. With int 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>& list says: I may change your vector.
  • const std::string& item says: I will look at your string, without copying it and without changing it.
  • int limit says: 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.

  1. Must the function change the caller's variable? Then a reference: T&.
  2. 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 an int, count is 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.

Your turn

Now try it yourself

Predict what a program will do, run it, and step through it line by line. The checks tell you when your own code works.

Loading the lab…

In this lab

  1. Two names, one variable

    This program declares a, then b and r from it. One of the two declarations has an & after the type.

    Decide what the program prints before you run it.

  2. The function that changes its caller

    add_bonus adds 50 to a score. Run the program: it prints Score: 120, the old score. The function changed its own copy.

    1. Press Step through and step into the call. The panel shows two variables named score, one for main and one for add_bonus. Only one of them becomes 170.
    2. Add one character to the first line of add_bonus, so that its parameter is main's variable and not a copy.
    3. Step through again. Now both names show the same value at every step, because there is one variable.

    Change nothing else. The program should print Score: 170. The checks try other scores.

  3. Look, do not touch

    reset_first takes a vector by const reference, and its body assigns to the first element.

    Decide what happens before you press Run.

  4. Double every element

    double_all is meant to double every element of the caller's vector. Its parameter is a reference already. Run the program: it prints 3, 8 and 5, unchanged.

    Make two changes.

    1. In double_all, the loop variable x is a copy of each element. Change the loop so that x is the element itself. The program then prints 6, 16 and 10.
    2. In print_all, change the loop so that x is a const reference to each element: it neither copies the element nor is able to change it.

    For int elements a copy costs no more than a reference, so the second change makes no difference you can measure. It matters when the elements are strings, and this is the place to practise the form.

    The checks call both functions with vectors of their own.

  5. Exchange two values

    exchange(a, b) is meant to exchange the values of the caller's two variables. Run the program: it prints 25 and 60, in the old order.

    The function is wrong twice.

    1. It works on copies. Repair that first, and run again. Before you run, say what you expect; the output is not yet right.
    2. It loses one of the two values. Press Step through and watch first and second after each statement of the body to see the moment the value is lost. Then repair the body.

    The program should print first: 60 and second: 25. Change only exchange. The checks call it with variables of their own.

  6. Two answers from one call

    smallest_and_largest looks through a vector. It has three parameters, and each is passed in a different way or for a different reason. Read its first line with care.

    main calls it with two variables that hold 0. Decide what the two printed lines show.

  7. clamp_all

    Write a function clamp_all above main.

    The call: clamp_all(v, low, high); where v is a vector of int, and low and high are ints. The caller promises that low is not greater than high.

    What it does: it changes the caller's vector so that every element lies between low and high. An element below low becomes low. An element above high becomes high. Every other element stays as it is. The function returns nothing and prints nothing.

    You choose how each of the three parameters is passed. main is given; with a correct clamp_all it prints 40, 100, 0, 100 and 7.

    The checks call clamp_all with vectors and limits that main never uses.

  8. longest_word

    Write a function longest_word above main.

    The call: longest_word(words) where words is a vector of strings.

    What it returns: the longest string in the vector, as a std::string. When several strings share the greatest length, it returns the first of them. For an empty vector it returns the empty string "".

    How it takes the vector: without copying it, and with a promise not to change it. The vector may hold thousands of words. Inside the function, do not copy each word to look at it either.

    The function returns its answer and prints nothing. main is given; it prints willow.

    The checks call longest_word with other vectors. One check looks at the type of the parameter, since no output can show whether a copy was made.

  9. sort_three

    Write two functions above main.

    exchange(a, b) exchanges the values of the caller's two int variables.

    sort_three(a, b, c) leaves the caller's three int variables in increasing order: afterwards a holds the smallest of the three values, c the largest and b the one between. Values may be equal. It does its work by calling exchange, each time for two variables that are the wrong way round.

    Both functions return nothing and print nothing. main is given; it prints 10 20 30.

    Before you write sort_three, decide how many comparisons it needs, and try your plan on paper with the values 3, 2, 1.

    The checks call both functions with variables of their own, in every order.

  10. A garden from a list

    Write a function draw_garden above main. It draws one row of flowers.

    The call: draw_garden(xs, y, colour);

    • xs is a vector of int: the x position of each flower in the row. The function only reads it.
    • y is an int: the height of the row on the canvas.
    • colour is a string: the colour of the petals. The function only reads it.

    Choose the kind of each parameter by the rule of the lesson: neither the vector nor the string is copied or changed, and the small number is passed by value.

    One flower at (x, y) is three shapes, drawn in this order.

    1. The stem: a "green" rectangle 6 wide and 70 high, with its top left corner at (x - 3, y)
    2. The petals: a circle with its centre at (x, y), radius 22, in the given colour
    3. The middle: a circle with its centre at (x, y), radius 9, "yellow"

    draw_garden draws one such flower for each element of xs, in the order of the vector. It does not call canvas.

    main is given. It draws two rows, seven flowers in all.

  11. remove_negatives

    Write a function remove_negatives above main.

    The call: remove_negatives(v); where v is a vector of int.

    What it does: it changes the caller's vector so that only the elements that are zero or more remain, in the order they were in. It returns nothing and prints nothing.

    You cannot take an element out of the middle of a vector with what you know. Build a second vector of the elements to keep, and then assign it to the first, as remove_all does in the lesson.

    main is given; it prints 12, 0 and 31.

  12. rotate_left

    Write a function rotate_left above main.

    The call: rotate_left(v); where v is a vector of int.

    What it does: it changes the caller's vector. Every element moves one position towards the front, and the element that was first goes to the end. So 5, 6, 7, 8 becomes 6, 7, 8, 5. The number of elements does not change.

    A vector with one element is unchanged. An empty vector stays empty: it has no first element, so the function must not read one.

    The function returns nothing and prints nothing. main is given; it prints 6, 7, 8 and 5.

  13. divide

    Write a function divide above main. It has two results and can fail, so it uses the pattern of first_negative in the lesson.

    The call: divide(a, b, quotient, remainder) where all four are ints. a and b are inputs. quotient and remainder are output parameters: variables of the caller that the function fills.

    What it returns: a bool.

    • When b is 0 the division cannot be done. The function returns false and changes nothing: both of the caller's variables keep the values they had.
    • Otherwise it stores the whole-number quotient of a divided by b in quotient and the remainder of that division in remainder, and returns true.

    The function prints nothing. main is given; it prints 47 / 5 is 9 remainder 2 and then cannot divide by 0.

  14. capitalise_all

    Write a function capitalise_all above main.

    The call: capitalise_all(words); where words is a vector of strings.

    What it does: it changes each string in the caller's vector so that its first character is a capital letter. The rest of each word stays as it is. A word that already starts with a capital, or with a character that is not a letter, is unchanged. An empty word stays empty: it has no first character.

    The function returns nothing and prints nothing. main is given; it prints River, Stone and Bridge.

  15. The exchange that did nothing

    This program is meant to print two numbers with the smaller one first. It prints 42 17, the order they started in.

    There are three functions: exchange, put_in_order and main. One of them is wrong, and one small repair to it is enough. The other two must stay exactly as they are.

    Do not guess. Press Step through and follow the values of smaller and larger into the calls. Find the first statement after which a value is not what it should be.

    The checks call exchange and put_in_order with variables of their own.

  16. Which kind of parameter?

    Six functions are described in the comments below. For each, write its first line only, followed by a semicolon, which makes it a declaration. Decide for every parameter whether it is passed by value, by reference or by const reference, and decide the return type.

    Under each first line, write a comment of one line for each parameter that gives your reason.

    Nothing checks this one. The program compiles as long as each line you add is a correct declaration. When you have written all six, go through the list below.