Lambdas and Callable Objects

Callbacks and Predicates

Sometimes we want one function to delegate the decision of what to do to another part of the code. Instead of hard coding the decision at compile time, we can write the first function so that it accepts another function (or callable) as an argument, and we can invoke that second callable whenever the decision needs to be made. The second callable function is called a callback.

A callback is a callable object passed as an argument that will be called from within that receiving function. Here's a trivial example.

#include <iostream>

void sayHello()
{
    std::cout << "hello\n";
}

// Define or declare this function with the shown parameters
void runTwice(void (*fn)())
{
    fn();
    fn();
}

int main()
{
    runTwice(sayHello);
    return 0;
}

sayHello prints a message. runTwice accepts one argument, an argument called fn which is itself a callable of void (no arguments, no return value). Inside runTwice, the parameter is called twice using the normal call syntax fn(). When main calls runTwice(sayHello), the call runTwice(sayHello) gives the name sayHello to fn.

Then, when we call fn() inside runTwice, we get a call to sayHello.

This means that runTwice calls sayHello twice. As you might expect, the program will print the message to the console two times. We need to highlight the fact that runTwice itself knows nothing about what function it's going to call. This information is given at call time.

A predicate is a special kind of callback. What distinguishes a predicate is that it answers a yes/no question. It must take zero or more arguments and return a bool. It is useful in all those cases where the algorithm or library function needs to test some condition.

Let's say we want to find the first even number in a sequence. A simple predicate to test for evenness can be an ordinary function.

#include <iostream>

bool isEven(int n)
{
    return n % 2 == 0;
}

int main()
{
    int values[]{ 11, 23, 44, 57, 68 };

    // Increment the value before the expression uses it
    for (int i = 0; i < 5; ++i)
    {
        if (isEven(values[i]))
        {
            std::cout << values[i] << '\n';
            break;
        }
    }

    return 0;
}

isEven takes an integer, and returns true when the integer is divisible by 2, false otherwise. The loop goes through each element in the array, and calls the predicate function on that element. As soon as the predicate returns true, that element is printed to the console, and the loop stops.

The output of this program will be 44.

We should note here that this same predicate may be given to any algorithm that wishes to have a test that returns a bool.

std::find_if is an algorithm that is designed precisely to do such a task for us. It takes two iterators and a predicate, and will return an iterator to the first element in the range which makes the predicate return true.

#include <algorithm>
#include <iostream>
#include <vector>

bool isEven(int n)
{
    return n % 2 == 0;
}

int main()
{
    std::vector v{ 11, 23, 44, 57, 68 };

    // Find the first element satisfying this predicate
    auto it{ std::find_if(v.begin(), v.end(), isEven) };

    if (it != v.end())
        std::cout << *it << '\n';
    else
        std::cout << "none found\n";

    return 0;
}

std::find_if iterates through the range from v.begin() to v.end() and calls isEven on each element. When isEven is called with 44 it returns true, and std::find_if returns an iterator pointing at 44, and stops iterating. We test if the iterator it has returned is equal to the end iterator and we do so by testing it for equality: it != v.end().

If they are not equal, then an even number has been found and we print it. If they were equal, then no even number was found and it would print the second message.

Since a predicate is nothing more than a callable that returns a bool, we can supply any of the callables that we've looked at so far. In the case that we only want the predicate for a single function call, we can replace the ordinary named function that provides the predicate to the function with a small lambda:

#include <algorithm>
#include <iostream>
#include <vector>

int main()
{
    std::vector v{ 11, 23, 44, 57, 68 };

    // Find the requested value and return its iterator
    auto it{ std::find_if(v.begin(), v.end(),
                          [](int n) { return n % 2 == 0; }) };

    if (it != v.end())
        std::cout << *it << '\n';
    else
        std::cout << "none found\n";

    return 0;
}

The predicate in this case is a small lambda function, [](int n) { return n % 2 == 0; } which is specified on the spot and passed as the third argument to std::find_if. It will behave in precisely the same way as the predicate function, and the output will again be 44. The callable only exists while that line of code is executed, which means there is no long lived function that might have to be dealt with later.

It's crucial that a callback/predicate has a certain signature. If we pass it an argument with the wrong signature, like passing a function that doesn't have the same number of arguments or which returns a value of the wrong type, we will end up with a compile time error. Additionally, when we plan to use a callable sometime in the future, we have to ensure that it remains valid and in scope until the last time that we might wish to use it.

Passing a temporary lambda to an algorithm that finishes before the end of the statement will work fine.

Storing the same lambda for much longer periods after the statement has been finished will lead to undefined behavior.

Callbacks, then, are a way to allow one function to defer an aspect of its behavior to some other part of the code which is supplied later on.

Exercise: Callbacks and PredicatesStarter code, hints and solutionOpenClose
Guided practiceBeginner · 10–15 min

Practice: Callbacks and Predicates

Work through the mission, reveal hints only when needed, and compare your result with the model solution.

Your challenge

After the first-match search, call isEven(9) directly and label its result. Explain how the predicate parameter differs from captured state.

Mission checklist

  1. Run the starter as a baseline, then predict the change requested in Your challenge.
  2. Make that change in your editor; use the lesson-specific hints if you need help.
  3. Compile and run your answer, then compare with the worked solution and explain the result.

Starter program

This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.

#include <iostream>

bool isEven(int n)
{
    return n % 2 == 0;
}

int main()
{
    int values[]{ 11, 23, 44, 57, 68 };

    // Increment the value before the expression uses it
    for (int i = 0; i < 5; ++i)
    {
        if (isEven(values[i]))
        {
            std::cout << values[i] << '\n';
            break;
        }
    }

    return 0;
}

Hints

Hint 1

isEven is the predicate: it returns true for a value divisible by two. The loop breaks at the first matching element, not after printing all matches.

Hint 2

Call isEven with 9 after the loop and print its Boolean return value. The named predicate gets its tested number through its parameter, not through a capture.

Worked solution

The original loop stops at the first even element, 44. The added direct predicate call uses 9 and returns false, printed as 0. isEven takes a parameter and captures no external state.

#include <iostream>

bool isEven(int n)
{
    return n % 2 == 0;
}

int main()
{
    int values[]{ 11, 23, 44, 57, 68 };

    // Increment the value before the expression uses it
    for (int i = 0; i < 5; ++i)
    {
        if (isEven(values[i]))
        {
            std::cout << values[i] << '\n';
            break;
        }
    }

    std::cout << "isEven(9): " << isEven(9) << '\n';
    return 0;
}

Example output

44
isEven(9): 0

0 comments

No comments yet. Be the first.

Add a comment

About CppLearn

CppLearn is an interactive, free reference for modern C++. In each lesson, you'll find out how the language works, rather than just lines of code that you can copy and paste. We cover concepts starting from beginner lessons on fundamentals and proceed to templates, move semantics, smart pointers, and areas that usually trap or scare C++ learners.