Chapter 9
Functions: Giving Work a Name
Parameters, return values, and why you would bother.
At the end of chapter 1 you printed a fortune card, and if you tried the stretch you printed a second one, which meant copying the border lines. Chapter 5’s mad-libs was four lines per question, then the same four lines again with a different word in them. Chapter 8’s stretch had you write the random-number machinery out in full.
Three times now the book has pointed at that and said “chapter 9”. Here it is.
Giving a name to two lines
Here is the fortune card again, with the border pulled out:
#include <iostream>
void print_border() {
std::cout << "+----------------------+\n";
}
int main() {
print_border();
std::cout << "| You will meet a bug. |\n";
print_border();
}
+----------------------+
| You will meet a bug. |
+----------------------+
print_border is a function. You have been looking at one since chapter 1:
main has exactly this shape, and now you know that shape wasn’t special to
main.
Reading it left to right: void is what it hands back, print_border is the
name, the empty brackets say it needs nothing from you, and the braces hold the
work. Down in main, print_border(); means do that now. The brackets are how
you call it; without them you have written the function’s name and asked for
nothing.
void is the new word. It means this function doesn’t answer a question, it just
does something. Plenty of useful work is like that: printing, saving, drawing.
The function ends when it runs out of body, and nothing comes back.
Order matters. print_border is written above main because the compiler
reads your file top to bottom and won’t call something it hasn’t met yet. Move it
below and you get:
error: use of undeclared identifier 'print_border'
For now: define your functions above main. Chapter 20 shows the other way, which
is how real programs stop caring about order.
Handing it something to work with
A function that always does the identical thing is only worth so much. Chapter 5’s problem was the same four lines with a different word each time.
The brackets are where the differences go:
void greet(std::string name, int times) {
for (int i = 0; i < times; ++i) {
std::cout << "Hello, " << name << "!\n";
}
}
int main() {
greet("Ada", 2);
greet("Grace", 1);
}
Hello, Ada!
Hello, Ada!
Hello, Grace!
name and times are parameters. They are declared like any other variable,
a type and a name, and they exist only while the function is running. When you
call greet("Ada", 2), the values in the call get copied into them, in order.
In order, and in the right number. Ask for one when it wants two:
error: no matching function for call to 'greet'
note: candidate function not viable: requires 2 arguments, but 1 was provided
Which is the compiler being unusually helpful. It tells you the count it wanted, the count it got, and points at the function it was trying to use.
Getting an answer back
void functions do work. The other kind answers a question:
int square(int n) {
return n * n;
}
int main() {
std::cout << square(7) << '\n';
int area = square(3) + square(4);
std::cout << area << '\n';
}
49
25
The int at the front is a promise: call me and you get an int back. return
is how the promise is kept, and it also ends the function immediately. Anything
written after it doesn’t run.
The interesting line is the second one. square(3) + square(4) works because a
call to a function that returns an int is an int, as far as the rest of the
line is concerned. You can add it, print it, store it, compare it, put it in an
if. You have been doing this since chapter 4 without noticing: full.size()
gives back a number, and you have been dropping it into std::cout and into
comparisons ever since.
Functions calling functions
Nothing says a function can only be called from main. A function can call
another one, and this is where the idea starts paying for itself:
int square(int n) {
return n * n;
}
int sum_of_squares(int a, int b) {
return square(a) + square(b);
}
int main() {
std::cout << sum_of_squares(3, 4) << '\n';
}
25
sum_of_squares doesn’t know how squaring works and doesn’t need to. It knows
there is something called square that takes an int and gives one back. If you
later decide squaring should print a warning for negative numbers, you change it
in one place and every caller gets the new behaviour.
The order rule still applies. square sits above sum_of_squares because
sum_of_squares calls it, and both sit above main. Written this way a file
reads bottom-up: the details first, the summary last. Chapter 20 hands you the
tool to stop caring about that.
The function gets a copy
Here is a thing that surprises everybody once:
void add_ten(int n) {
n = n + 10;
std::cout << "inside: " << n << '\n';
}
int main() {
int score = 5;
add_ten(score);
std::cout << "outside: " << score << '\n';
}
inside: 15
outside: 5
score did not change. When you call add_ten(score), the value 5 is copied
into n. n is a new variable that happens to start with the same contents.
Changing it changes nothing outside.
This is the default, and it is mostly what you want, since a function can’t quietly
wreck your variables. When you genuinely need a function to modify what you passed
it, there is a way, and chapter 12 is the whole chapter about it. Until then: if
you want a changed value back, return it.
Names stop at the braces
A variable declared inside a function belongs to that function:
int square(int n) {
int answer = n * n;
return answer;
}
int main() {
std::cout << square(7) << '\n';
std::cout << answer << '\n'; // error
}
error: use of undeclared identifier 'answer'
Not a rule to memorise so much as a relief. It’s why you can call a parameter n
in one function and n in another and they never collide, and why you can use i
in every for loop you write without keeping a list. The same goes for the loop
variable itself, which stops existing at the loop’s closing brace.
This is the real argument for functions, and it isn’t “less typing”. A function you can read in ten seconds, whose inputs are listed in its brackets and which can’t touch anything else, is a piece of the program you can stop thinking about. Programs get big. That is how you survive it.
Exercise 1 · Three small ones
Write int double_it(int n) that returns n * 2. Call it from main and print
the result.
Write void countdown(int from) that prints from down to 1 using a for loop.
Call it with 5, then with 3.
Now break it deliberately: take the return out of double_it and compile.
Read the warning, put it back, and note that the build succeeded either way.
Check yourself
Project
The guessing game, rebuilt, then a pair of dice
Roughly 45 minutes
Part one: take chapter 8’s game apart.
The game works. You are not fixing it, you are pulling two jobs out of main
and giving them names:
int read_guess() {
int guess = 0;
std::cout << "Your guess: ";
std::cin >> guess;
return guess;
}
void report(int guess, int secret) {
if (guess < secret) {
std::cout << "Higher.\n";
} else if (guess > secret) {
std::cout << "Lower.\n";
}
}Now the loop in main is three lines: read a guess, count it, report on it.
while (guess != secret) {
guess = read_guess();
++attempts;
report(guess, secret);
}Note that read_guess is a prompt and a read together, because those two things
are never useful apart. And report has no else for the correct guess, for the same
reason as chapter 8, the loop ending is what means they got it.
Read the whole thing top to bottom afterwards. It is about the same number of
lines as before, and main now says what the game is rather than how each
piece works. That is the trade, and it is the only honest argument for functions:
not less code, less to hold in your head at once.
Part two: dice.
Chapter 8 gave you three borrowed lines for a random number. Give them a name:
#include <random>
int roll(int sides) {
std::random_device seed;
std::mt19937 generator(seed());
std::uniform_int_distribution<int> between(1, sides);
return between(generator);
}The machinery is unchanged from chapter 8. It has moved somewhere with a name, and gained a parameter so it can roll a die of any size.
Write a main that rolls two six-sided dice, prints both, and prints the total.
Then roll(20) for a twenty-sided one, from the same function, with nothing
rewritten.
Stretch: roll until you get doubles. Loop, rolling two dice each time,
counting the rounds, stopping when they match. Everything you need is now in
three named pieces plus a while.