Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 6 of 6

Functions live in memory, so they have addresses, so you can point at them. That turns behaviour into something you can store in a variable and pass to a function.

#include <iostream>

int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}

int main() {
    int (*op)(int, int) = add;      // op points at add

    std::cout << op(3, 4) << "\n";

    op = multiply;                  // now it points at multiply
    std::cout << op(3, 4) << "\n";
    return 0;
}
7
12

Reading the declaration

int (*op)(int, int);
 ^   ^        ^
 |   |        └── the parameter types it takes
 |   └── op is a pointer
 └── it returns int

The parentheses around *op are load-bearing. Drop them and the meaning changes completely:

int (*op)(int, int);    // pointer to a function taking (int, int), returning int
int *op(int, int);      // a FUNCTION taking (int, int), returning int*

The second is a function declaration, not a variable. This is the single most common mistake with function pointers, and the compiler's error message when you then try to assign to it is not especially helpful.

using makes it readable, and is what you should actually write:

#include <iostream>

using BinaryOp = int (*)(int, int);

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }

int main() {
    BinaryOp op = add;
    std::cout << op(10, 4) << "\n";

    op = subtract;
    std::cout << op(10, 4) << "\n";
    return 0;
}
14
6

Two spellings that mean the same thing

A function name decays to a pointer on its own, and calling through a pointer needs no dereference — so the explicit forms and the plain forms are interchangeable:

#include <iostream>

int square(int n) { return n * n; }

int main() {
    int (*a)(int) = square;      // plain
    int (*b)(int) = &square;     // explicit address-of

    std::cout << (a == b) << "\n";
    std::cout << a(5) << " " << (*a)(5) << "\n";   // both call it
    return 0;
}
1
25 25

Write the plain forms. The explicit ones are legal and occasionally show up in older code.

The point: passing behaviour

#include <iostream>

void apply_to_all(int* values, std::size_t count, int (*transform)(int)) {
    for (std::size_t i = 0; i < count; ++i) {
        values[i] = transform(values[i]);
    }
}

int square(int n) { return n * n; }
int negate(int n) { return -n; }

int main() {
    int numbers[5] = {1, 2, 3, 4, 5};

    apply_to_all(numbers, 5, square);
    for (int n : numbers) std::cout << n << " ";
    std::cout << "\n";

    apply_to_all(numbers, 5, negate);
    for (int n : numbers) std::cout << n << " ";
    std::cout << "\n";
    return 0;
}
1 4 9 16 25 
-1 -4 -9 -16 -25 

apply_to_all does not know what it is doing to each element. The caller decides. This is the idea behind the whole <algorithm> header (Module 13): std::sort takes a comparison, std::find_if takes a predicate, and so on.

Where this is going: callbacks and comparators

#include <iostream>

bool ascending(int a, int b) { return a < b; }
bool descending(int a, int b) { return a > b; }

// A tiny selection sort that takes its ordering as an argument.
void sort_with(int* values, std::size_t count, bool (*before)(int, int)) {
    for (std::size_t i = 0; i < count; ++i) {
        std::size_t best = i;
        for (std::size_t j = i + 1; j < count; ++j) {
            if (before(values[j], values[best])) {
                best = j;
            }
        }
        int temp = values[i];
        values[i] = values[best];
        values[best] = temp;
    }
}

int main() {
    int numbers[6] = {5, 2, 9, 1, 7, 3};

    sort_with(numbers, 6, ascending);
    for (int n : numbers) std::cout << n << " ";
    std::cout << "\n";

    sort_with(numbers, 6, descending);
    for (int n : numbers) std::cout << n << " ";
    std::cout << "\n";
    return 0;
}
1 2 3 5 7 9 
9 7 5 3 2 1 

One sort, two orderings, decided by the caller. std::sort works exactly this way.

The limitation, and what replaces it

A function pointer can only point at a plain function. It cannot capture anything:

int factor = 3;

// You cannot write a function pointer that multiplies by `factor` without
// making `factor` a global or passing it through as another parameter.

That restriction is why lambdas exist (lesson 14.1). A lambda can capture surrounding variables and still be passed as an argument:

#include <iostream>

int main() {
    int factor = 3;

    // Captures factor. A plain function pointer could not do this.
    auto scale = [factor](int n) { return n * factor; };

    std::cout << scale(5) << "\n";
    return 0;
}
15

In modern C++ you will write lambdas nearly every time. Function pointers still matter for reading C APIs, callbacks in older libraries, and understanding what a lambda without captures converts to.

Try it

Write a four-function calculator that stores add, subtract, multiply and divide in an array of function pointers, and dispatches on an index rather than a switch.

Checkpoint

  • Functions have addresses; int (*op)(int, int) is a pointer to one.
  • The parentheses matter: int *op(int, int) declares a function, not a pointer.
  • using aliases make these declarations readable — use them.
  • A function name decays to a pointer, and calling through one needs no *.
  • Passing a function as an argument lets one algorithm take its behaviour from the caller.
  • Function pointers cannot capture; that is what lambdas are for.

Questions about this lesson

Log in to ask a question.