Function Pointers, Functors and Lambdas in C++

C++ supports a variety of function-like entities, including function pointersfunctors, and lambdas. Among these, function pointers are the most traditional, originating from the C programming language—the precursor to C++. At the other end of the spectrum are lambdas, introduced in C++11, which have evolved significantly in power and flexibility in subsequent C++ standards.

Function Pointers

We’ll begin with function pointers, the oldest form of callable abstraction in C++. A function pointer, like any pointer, holds the address of something in memory—in this case, the entry point of a function. The name of a function itself can be used as a function pointer literal, as it evaluates to the address of the function. We can create function pointer variables and assign values to them like any other pointer variable.

You can declare function pointer variables and assign them just like other pointers. The syntax requires some care: the declaration must include an asterisk (*) before the variable name to indicate it’s a pointer, and both the asterisk and the variable name must be enclosed in parentheses. This grouping ensures the asterisk applies to the variable—not to the function’s return type. This syntax must be followed by a set of parenthesis, and potentially an argument list, that indicates the pointer is a pointer to a function.

#include <iostream>

void my_function() {
    std::cout << "Hello World!" << std::endl;
}

void (*func_ptr)() = my_function;

int main() {
    my_function();
    func_ptr();
    return 0;
}
Hello World!
Hello World!

Notice in the example how we can invoke the function my_function using the name of the function, which is a function pointer literal, or by using the function pointer variable func_ptr simply by adding an argument list after either.

You can also declare function pointers to functions that take arguments and return a value.

#include <iostream>

double fp_div(int n, int d) {
    return n / (double) d;
}

double (*fp_div_ptr)(int n, int d) = fp_div;

int main() {
    int a = 10;
    int b = 3;
    std::cout << a << " / " << b << " = " << fp_div(a, b) << std::endl;
    std::cout << a << " / " << b << " = " << fp_div_ptr(a, b) << std::endl;
    return 0;
}
10 / 3 = 3.33333
10 / 3 = 3.33333

These trivial examples don't really show the power of function pointers. Where they really become useful is when you have a function that takes an argument that is a function pointer. This technique allows us to write a more generic function that takes a function pointer argument to supply the specific code for each specific invocation of the algorithm.

#include <iostream>

int int_mult(int n, int d) {
    return n * d;
}

int int_div(int n, int d) {
    return n / d;
}

// print_operation takes a function pointer argument (called op_func) to a
// function that takes two ints as arguments and returns an int.
void print_operation(int (*op_func)(int, int), int a, int b) {
    std::cout << a << " op " << b << " = " << op_func(a, b) << std::endl;
}

int main() {
    int a = 10;
    int b = 3;
    print_operation(&int_mult, a, b);
    print_operation(&int_div, a, b);
    return 0;
}
10 op 3 = 30
10 op 3 = 3

Another example is a sorting algorithm. We can write a sort function that handles the common logic for all sorting. We design it so that it takes a comparison function pointer as an argument so that we can either sort ascending or descending.

#include <iostream>

bool is_less(int a, int b) {
    return a < b;
}

bool is_greater(int a, int b) {
    return a > b;
}

// Sort takes an array and its size plus a boolean function pointer
// (called cmp_func) that will perform the comparison of adjacent elements
// in the array.
void sort(int array[], int count, bool (*cmp_func)(int, int)) {
    for (int i = 0; i < count; ++i) {
        for (int j = i + 1; j < count; ++j) {
            if (cmp_func(array[j], array[i])) {
                std::swap(array[i], array[j]);
            }
        }
    }
}

int main() {
    int int_array[10];
    for (int i = 0; i < 10; ++i) {
        int_array[i] = rand() % 100;
    }
    // sort ascending with is_less()
    sort(int_array, std::size(int_array), is_less);
    for (int i : int_array) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    // sort descending with is_greater()
    sort(int_array, std::size(int_array), is_greater);
    for (int i : int_array) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
    return 0;
}
7 9 23 30 44 49 58 72 73 78 
78 73 72 58 49 44 30 23 9 7

Because the comparison function is typed so that it must take two integer arguments, this limits our sort() method to sorting integer arrays only. If we want to make the sort() method more generic, we have to pass functions that take untyped pointers, namely void pointers (void*). Using this technique we can not only create a truly generic sort() method but also a generic print_array() function that can print any array with the help of a pointer to a function that prints one element.

#include <iostream>

bool int_is_less(void *a, void *b) {
    return *(int*)a < *(int*)b;
}

bool int_is_greater(void *a, void *b) {
    return *(int*)a > *(int*)b;
}

bool dbl_is_less(void *a, void *b) {
    return *(double*)a < *(double*)b;
}

bool dbl_is_greater(void *a, void *b) {
    return *(double*)a > *(double*)b;
}

void sort(void *array, int count, size_t elem_size, bool (*cmp_func)(void*, void*)) {
    char temp[elem_size];
    for (int i = 0; i < count; ++i) {
        for (int j = i + 1; j < count; ++j) {
            if (cmp_func((char*)array + j * elem_size, (char*)array + i * elem_size)) {
                memcpy(temp, (char *) array + i * elem_size, elem_size);
                memcpy((char *) array + i * elem_size,
                       (char *) array + j * elem_size, elem_size);
                memcpy((char *) array + j * elem_size, temp, elem_size);
            }
        }
    }
}

void print_array(void *array, int count, size_t elem_size, void (*print_func)(void *)) {
    void *elem_ptr = array;
    for (int i = 0; i < count; ++i) {
        print_func(elem_ptr);
        std::cout << " ";
        elem_ptr = (char*)elem_ptr + elem_size;
    }
    std::cout << std::endl;
}

void print_int(void *i) {
    std::cout << *(int *)i;
}

void print_dbl(void *i) {
    std::cout << *(double *)i;
}

int main() {
    int int_array[10];
    for (int i = 0; i < 10; ++i) {
        int_array[i] = rand() % 100;
    }
    // sort ascending with int_is_less()
    sort(int_array, sizeof(int_array) / sizeof(int_array[0]),
        sizeof(int_array[0]), int_is_less);
    print_array(int_array, sizeof(int_array) / sizeof(int_array[0]),
        sizeof(int_array[0]), print_int);
    // sort descending with int_is_greater()
    sort(int_array, sizeof(int_array) / sizeof(int_array[0]),
        sizeof(int_array[0]), int_is_greater);
    print_array(int_array, sizeof(int_array) / sizeof(int_array[0]),
        sizeof(int_array[0]), print_int);

    double dbl_array[10];
    for (int i = 0; i < 10; ++i) {
        dbl_array[i] = rand() / (double)RAND_MAX;;
    }
    // sort ascending with dbl_is_less()
    sort(dbl_array, std::size(dbl_array),
        sizeof(dbl_array[0]), dbl_is_less);
    print_array(dbl_array, std::size(dbl_array),
        sizeof(dbl_array[0]), print_dbl);
    // sort descending with dbl_is_greater()
    sort(dbl_array, std::size(dbl_array),
        sizeof(dbl_array[0]), dbl_is_greater);
    print_array(dbl_array, std::size(dbl_array),
        sizeof(dbl_array[0]), print_dbl);
    return 0;
}
7 9 23 30 44 49 58 72 73 78 
78 73 72 58 49 44 30 23 9 7
0.00769819 0.0345721 0.0534616 0.0668422 0.383416 0.383502 0.519416 0.5297 0.671149 0.830965
0.830965 0.671149 0.5297 0.519416 0.383502 0.383416 0.0668422 0.0534616 0.0345721 0.00769819

One of the downsides to this approach is that we much add an additional argument to the generic functions to tell them the size of our array elements. This is because in order to do array arithmetic with void pointers we must do all the computations manually using the size of elements passed in. Another downside is that we have to create helper functions for each different type we want to use. The whole thing ends up being sort of a mess.

Templates are a tool that we can use to eliminate a lot of the clutter by auto-generating specific functions for each type we need. We can also use template function pointers as arguments to our generic functions sort() and print_array(). This will cause specific versions of these functions to be instantiated. Using templates also allows us to use the template type directly when coding avoiding the need for the use of void pointers.

#include <iostream>

template<class T>
bool is_less(T a, T b) {
    return a < b;
}

template<class T>
bool is_greater(T a, T b) {
    return a > b;
}

template<class T>
void sort(T *array, int count, bool (*cmp_func)(T, T)) {
    for (int i = 0; i < count; ++i) {
        for (int j = i + 1; j < count; ++j) {
            if (cmp_func(array[j], array[i])) {
                std::swap(array[i], array[j]);
            }
        }
    }
}

template<class T>
void print_element(T i) {
    std::cout << i;
}

template<class T>
void print_array(T *array, int count, void (*print_func)(T)) {
    for (int i = 0; i < count; ++i) {
        print_func(array[i]);
        std::cout << " ";
    }
    std::cout << std::endl;
}

int main() {
    int int_array[10];
    for (int i = 0; i < 10; ++i) {
        int_array[i] = rand() % 100;
    }
    // sort ascending with is_less()
    sort(int_array, std::size(int_array), is_less<int>);
    print_array(int_array, std::size(int_array), print_element<int>);
    // sort descending with is_greater()
    sort(int_array, std::size(int_array), is_greater<int>);
    print_array(int_array, std::size(int_array), print_element<int>);

    double dbl_array[10];
    for (int i = 0; i < 10; ++i) {
        dbl_array[i] = rand() / (double)RAND_MAX;;
    }
    // sort ascending with is_less()
    sort(dbl_array, std::size(dbl_array), is_less<double>);
    print_array(dbl_array, std::size(dbl_array), print_element<double>);
    // sort descending with is_greater()
    sort(dbl_array, std::size(dbl_array), is_greater<double>);
    print_array(dbl_array, std::size(dbl_array), print_element<double>);
    return 0;
}
7 9 23 30 44 49 58 72 73 78 
78 73 72 58 49 44 30 23 9 7
0.00769819 0.0345721 0.0534616 0.0668422 0.383416 0.383502 0.519416 0.5297 0.671149 0.830965
0.830965 0.671149 0.5297 0.519416 0.383502 0.383416 0.0668422 0.0534616 0.0345721 0.00769819

Functors

While templates make the code more compact and cleaner, we are still passing function pointers around. Because these are addresses we have the opportunity to make errors, passing invalid pointers or the nullptr. We can make the code more safe by passing objects called functors instead of function pointers. Functors are classes that define the function call operator (operator()) allowing us to use functor objects like a function without the danger of using bad pointers.

#include <iostream>

// Base functor class that we will use as argument types.
template<class T>
class CompareFunctor {
public:
    virtual ~CompareFunctor() = default;
    // Derived classes must override the function call operator.
    virtual bool operator()(T a, T b) const = 0;
};

// Derived functor class to compute less than.
template<class T>
class LessFunctor : public CompareFunctor<T> {
    bool operator()(T a, T b) const override {
        return a < b;
    }
};

// Derived functor class to compute greater than.
template<class T>
class GreaterFunctor : public CompareFunctor<T> {
    bool operator()(T a, T b) const override{
        return a > b;
    }
};

template<class T>
void sort(T *array, int count, const CompareFunctor<T> &cmp_functor) {
    for (int i = 0; i < count; ++i) {
        for (int j = i + 1; j < count; ++j) {
            if (cmp_functor(array[j], array[i])) {
                std::swap(array[i], array[j]);
            }
        }
    }
}

// Functor class to print array elements.
template<class T>
class PrintFunctor {
public:
    void operator()(T i) const {
        std::cout << i;
    }
};

template<class T>
void print_array(T *array, int count, const PrintFunctor<T> &print_functor) {
    for (int i = 0; i < count; ++i) {
        print_functor(array[i]);
        std::cout << " ";
    }
    std::cout << std::endl;
}

int main() {
    int int_array[10];
    for (int i = 0; i < 10; ++i) {
        int_array[i] = rand() % 100;
    }
    // sort ascending with object of type LessFunctor<int>
    sort(int_array, std::size(int_array), LessFunctor<int> {});
    print_array(int_array, std::size(int_array), PrintFunctor<int> {});
    // sort descending with object of type GreaterFunctor<int>
    sort(int_array, std::size(int_array), GreaterFunctor<int> {});
    // Print with object of type PrintFunctor<int>
    print_array(int_array, std::size(int_array), PrintFunctor<int> {});

    double dbl_array[10];
    for (int i = 0; i < 10; ++i) {
        dbl_array[i] = rand() / (double)RAND_MAX;;
    }
    // sort ascending with object of type LessFunctor<double>
    sort(dbl_array, std::size(dbl_array), LessFunctor<double> {});
    // Print with object of type PrintFunctor<double>
    print_array(dbl_array, std::size(dbl_array), PrintFunctor<double> {});
    // sort descending with object of type GreaterFunctor<double>
    sort(dbl_array, std::size(dbl_array), GreaterFunctor<double> {});
    // Print with object of type PrintFunctor<double>
    print_array(dbl_array, std::size(dbl_array), PrintFunctor<double> {});
    return 0;
}
7 9 23 30 44 49 58 72 73 78 
78 73 72 58 49 44 30 23 9 7
0.00769819 0.0345721 0.0534616 0.0668422 0.383416 0.383502 0.519416 0.5297 0.671149 0.830965
0.830965 0.671149 0.5297 0.519416 0.383502 0.383416 0.0668422 0.0534616 0.0345721 0.00769819

Notice that the sort() and print_array() functions accept a reference to functor objects. This makes it impossible to pass a bad function like we can with function pointers. Another thing that we can do with functors is store state information in the objects, something that we cannot do with function pointers. For example, we can modify this program to allow the print functor to store the precision used to output the values.

#include <iostream>
#include <iomanip>

// Base functor class that we will use as argument types.
template<class T>
class CompareFunctor {
public:
    virtual ~CompareFunctor() = default;
    // Derived classes must override the function call operator.
    virtual bool operator()(T a, T b) const = 0;
};

// Derived functor class to compute less than.
template<class T>
class LessFunctor : public CompareFunctor<T> {
    bool operator()(T a, T b) const override {
        return a < b;
    }
};

// Derived functor class to compute greater than.
template<class T>
class GreaterFunctor : public CompareFunctor<T> {
    bool operator()(T a, T b) const override{
        return a > b;
    }
};

template<class T>
void sort(T *array, int count, const CompareFunctor<T> &cmp_functor) {
    for (int i = 0; i < count; ++i) {
        for (int j = i + 1; j < count; ++j) {
            if (cmp_functor(array[j], array[i])) {
                std::swap(array[i], array[j]);
            }
        }
    }
}

// Functor class to print array elements.
template<class T>
class PrintFunctor {
public:
    explicit PrintFunctor(int precision = 6) : precision_(precision) {}
    void operator()(T i) const {
        std::cout << std::setprecision(precision_) << i;
    }
private:
    int precision_;
};

template<class T>
void print_array(T *array, int count, const PrintFunctor<T> &print_functor) {
    for (int i = 0; i < count; ++i) {
        print_functor(array[i]);
        std::cout << " ";
    }
    std::cout << std::endl;
}

int main() {
    int int_array[10];
    for (int i = 0; i < 10; ++i) {
        int_array[i] = rand() % 100;
    }
    // sort ascending with object of type LessFunctor<int>
    sort(int_array, std::size(int_array), LessFunctor<int> {});
    // Print with object of type PrintFunctor<int>
    print_array(int_array, std::size(int_array), PrintFunctor<int> {});
    // sort descending with object of type GreaterFunctor<int>
    sort(int_array, std::size(int_array), GreaterFunctor<int> {});
    // Print with object of type PrintFunctor<int>
    print_array(int_array, std::size(int_array), PrintFunctor<int> {});

    double dbl_array[10];
    for (int i = 0; i < 10; ++i) {
        dbl_array[i] = rand() / (double)RAND_MAX;;
    }
    // sort ascending with object of type LessFunctor<double>
    sort(dbl_array, std::size(dbl_array), LessFunctor<double> {});
    // Print with object of type PrintFunctor<double> and precision 3
    print_array(dbl_array, std::size(dbl_array), PrintFunctor<double> {3});
    // sort descending with object of type GreaterFunctor<double>
    sort(dbl_array, std::size(dbl_array), GreaterFunctor<double> {});
    // Print with object of type PrintFunctor<double> and precision 1
    print_array(dbl_array, std::size(dbl_array), PrintFunctor<double> {1});
    return 0;
}
7 9 23 30 44 49 58 72 73 78 
78 73 72 58 49 44 30 23 9 7
0.0077 0.0346 0.0535 0.0668 0.383 0.384 0.519 0.53 0.671 0.831
0.8 0.7 0.5 0.5 0.4 0.4 0.07 0.05 0.03 0.008

So in many ways functors are a big improvement over function pointers. So much so, that they are widely used in the C++ Standard Template Library (STL). There is even a more advanced sort() function in the STL that we can use instead of rolling our own as we have done here.

Lambdas

In C++11, a powerful new feature called lambdas was introduced. Lambdas are similar to functors in that they can capture and retain state, but they use a much more concise syntax and can be defined inline at the point of use—eliminating the need to define a separate class.

A lambda expression can be used anywhere a functor or function pointer is valid. The syntax consists of three components:

[capture](arguments){code}
  • [capture]: Specifies which variables from the surrounding scope should be captured by the lambda. This can be empty ([]) if no external state is needed.
  • (arguments): Defines the input arguments, just like a regular function.
  • { body }: Contains the code to execute when the lambda is called.

This inline, flexible structure makes lambdas ideal for short, self-contained operations—especially in algorithms, callbacks, and multi-threaded code. In this trivial example, we create a lambda and assign it to a variable. Then we use that variable to call the lambda just like we call an ordinary function.

#include <iostream>

auto fp_div = [](int n, int d){ return n / (double)d; };

int main() {
    int a = 10;
    int b = 3;
    std::cout << a << " / " << b << " = " << fp_div(a, b) << std::endl;
    return 0;
}

Where lambdas really shine is when used as arguments to a function. Here is our sort() program example written with lambdas.

#include <iostream>
#include <iomanip>

template<typename T, typename Compare>
void sort(T *array, int count, Compare comparator) {
    for (int i = 0; i < count; ++i) {
        for (int j = i + 1; j < count; ++j) {
            if (comparator(array[j], array[i])) {
                std::swap(array[i], array[j]);
            }
        }
    }
}

template<typename T, typename Print>
void print_array(T *array, int count, Print printer) {
    for (int i = 0; i < count; ++i) {
        printer(array[i]);
        std::cout << " ";
    }
    std::cout << std::endl;
}

int main() {
    int int_array[10];
    for (int i = 0; i < 10; ++i) {
        int_array[i] = rand() % 100;
    }
    // sort ascending with object of type LessFunctor<int>
    sort(int_array, std::size(int_array),
        [](int a, int b) { return a < b; });
    // Print with object of type PrintFunctor<int>
    print_array(int_array, std::size(int_array),
        [](int a) { std::cout << a; });
    // sort descending with object of type GreaterFunctor<int>
    sort(int_array, std::size(int_array),
        [](int a, int b) { return a > b; });
    // Print with object of type PrintFunctor<int>
    print_array(int_array, std::size(int_array),
        [](int a) { std::cout << a; });

    double dbl_array[10];
    for (int i = 0; i < 10; ++i) {
        dbl_array[i] = rand() / (double)RAND_MAX;;
    }
    // sort ascending with object of type LessFunctor<double>
    sort(dbl_array, std::size(dbl_array),
        [](double a, double b) { return a < b; });
     // Print with object of type PrintFunctor<double> and precision 3
    int precision = 3;
    print_array(dbl_array, std::size(dbl_array),
        [precision](double a) { std::cout << std::setprecision(precision) << a; });

    // sort descending with object of type GreaterFunctor<double>
    sort(dbl_array, std::size(dbl_array),
        [](double a, double b) { return a > b; });
    // Print with object of type PrintFunctor<double> and precision 1
    precision = 1;
    print_array(dbl_array, std::size(dbl_array),
        [precision](double a) { std::cout << std::setprecision(precision) << a; });
    return 0;
}
7 9 23 30 44 49 58 72 73 78 
78 73 72 58 49 44 30 23 9 7
0.0077 0.0346 0.0535 0.0668 0.383 0.384 0.519 0.53 0.671 0.831
0.8 0.7 0.5 0.5 0.4 0.4 0.07 0.05 0.03 0.008

With lambdas there is a lot less boilerplate code. We just declare lambdas when we need them. It also allows us to customize the code more. Notice how the lambdas for the print_array() calls for the integer arrays don't capture anything but the print_array() calls for the double arrays captures an outer-scoped precision variable to help format the doubles.

It turns out that you can capture variables from the outer-scope of a lambda as copies by value or by reference so that the lambda can make changes to the outer-scope variables. Just naming a variable in the capture square brackets will capture a copy, i.e. by value. If you prefix the variable with an ampersand it changes the capture to by reference.

#include <iostream>

int main() {
    int counter = 0;
    // Can't change variable captured by value
    //auto capture_by_value = [counter](){ std::cout << ++counter << std::endl; };
    auto capture_by_reference = [&counter](){ std::cout << ++counter << std::endl; };
    capture_by_reference();
    capture_by_reference();
    capture_by_reference();
    return 0;
}
1
2
3

You can list variables in the capture brackets to specify how each is captured—by value or by reference. To capture all outer-scope variables by value, use a single equals sign (=); to capture all by reference, use a single ampersand (&). You can also mix capture modes by combining = or & with a list of exceptions. For example, [=, &x] captures all variables by value except x, which is captured by reference.

#include <iostream>

int main() {
    int i = 0;
    int j = 0;
    int k = 0;
    auto print_by_value = [=](){ std::cout << i << " " << j << " " << k << std::endl; };
    auto print_by_reference = [&](){ std::cout << i << " " << j << " " << k << std::endl; };
    auto all_by_reference = [&](){ ++i; j += 2; k += 3; };
    auto mixed = [=, &j, &k](){ ++j; k += i; };
    all_by_reference();
    all_by_reference();
    print_by_value();
    print_by_reference();
    mixed();
    mixed();
    print_by_value();
    print_by_reference();
    return 0;
}
0 0 0
2 4 6
0 0 0
2 6 6

Notice how the print_by_value() function always prints zeros. That is because i, j and k were 0 when they were captured by value when the lambda was defined. The print_by_reference() and all_by_reference() methods, however, can access the state of those variables when the lambda is used and can change those variables. It is perhaps a little surprising what the mixed() function does. Because i is captured by value, it will always be 0 so k doesn't increase even though it was captured by reference.