Contents

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

1. What does void mean at the front of a function?

Not quite. That is what the empty brackets say. The word at the front is about what comes back, not what goes in.

Yes. It does work rather than answering a question, so there is no return value to use in an expression.

Not quite. A void function can do a great deal. It just has no answer for you when it finishes.

2. A function changes its parameter. What happens to the variable that was passed in?

Yes. The value is copied into the parameter at the call. Chapter 12 covers the case where you want the original changed.

Not quite. It has the same contents at the start and a different name. Print it after the call and you will see it untouched.

Not quite. Unrelated. Returning is how you hand an answer back; it has no effect by itself on the variables you passed in.

3. Why can square(3) + square(4) be written on one line?

Not quite. Nothing here is simultaneous. They run one after the other, and then the results are added.

Yes. Once it has returned, the call is just a number as far as the rest of the line is concerned: addable, printable, comparable.

Not quite. Irrelevant. Two calls to different int functions would add together just as happily.

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.