Motivation for Callbacks

Imagine that you have a long-running algorithm which takes many iterations to complete. Typically, you will want to provide some kind of feedback to the user to indicate that progress is being made. Otherwise, there is no way of distinguishing between an application that is happliy crunching numbers, and one that is hanging on a dropped network connection. Importantly, not all users will require the same type of feedback. An update could be just about anything you could think of, including:

  • Incrementing an iteration counter at the terminal.
  • Updating a progress bar.
  • Writing intermediate results to disk.
  • Updating a user interface.

The important thing to take away is that different users will have different requirements, and it is difficult or impossible for the author of the algorithm to anticipate all possible actions a user might need.

Callbacks are an ideal paradigm for dealing with this problem. At a high level, the user defines some programatic action which can be added to the algorithm and called at a specified time. This provides quite a bit of flexibility to the user, who can divise any callback they wish, without needing access to the algorithm’s source code. In c++, callbacks are stored as “callables”: i.e., function pointers, pointers to class methods, functors (classes which overload operator()), and lambda functions (since c++11).

Prior to c++11, this was a relatively intimidating topic, since the syntax of function pointers and pointers to class methods involved complicated and unintuitive syntax. However, the advent of the auto keyword, as well as lambda functions, has greatly simplified this topic. In this tutorial, we will cover a very simple callback example in c++11. All examples were compiled with g++ 7.2.0 on Ubuntu 16.04.

A Toy Example

To begin, we define a very simple class, called SquareRoot, containing a single method, double SquareRoot::run(const double), which iteratively approximates the square root of its input using the Babylonian method.

In int main(), we instantiate the class, and call run() with an example input. (We use 1234.5*1234.5 as an input, because we know that the correct output should be 1234.5.)

// Includes
#include <cstdlib>
#include <iostream>
#include <math.h>

// Class with callback
class SquareRoot {
public:
  // Main logic of class.
  double run(const double input) {
    if (input < 0.0) throw 0; // Error checking.
    this->iteration = 0;      // Reset iteration number.
    double guess = input;     // Set initial guess to input.
    // Babylonian method.
    while (std::fabs(guess - input/guess) > this->epsilon) {
      guess = (guess + input / guess) / 2.0;
      ++iteration;
    }
    return guess;
  }

private:
  const double epsilon = 1e-6; // Maximum Allowed Error.
  size_t iteration = 0;        // Iteration Number.
};

int main() {

  SquareRoot p;
  std::cout << "Result: " << p.run(1234.5*1234.5) << std::endl;

  return EXIT_SUCCESS;

}

Let’s save the code as callbacks.cxx, compile, and run:

$ g++ ./callbacks.cxx
$ ./a.out
Result: 1234.5

Success! But what if we want to get more information about how the algorithm is running, such as printing out intermediate guesses? For that, we’ll use callbacks.

Defining a Callback Mechanism

Let’s now define our callback mechanism. For this simple example, our callbacks will take the iteration index and to the intermediate guess (const size_t, const double), and return void. To do this, we will use the std::function template (defined in the <functional> header):

using TCallback = std::function<void(const size_t, const double)>;

A TCallback instance can be used to store all the callables described above: function pointers, pointers to class methods, functors, and lambda functions. Since a user may want to add more than one callback, it is generally a good idea to store a std::vector of callbacks, rather than a single one. The type of the callback vector is defined as follows:

using TCallbackVector = std::vector<TCallback>;

We can then define a private data member to hold the callback vector…

private:
  // Data member holding callbacks.
  TCallbackVector m_callbacks;

…and a class method to add callbacks to the vector.

// Push callbacks onto the stack.
void add_callback(TCallback cb) {
  m_callbacks.push_back(cb);
}

Finally, we add logic to the Program::run method, which invokes each callback prior to each update:

// Main logic of class, where callbacks are invoked.
void run() {
  // ...
  for (const auto &cb : m_callbacks) {
    cb(iteration, guess);
  }
  // ...
}

We can now put it all together into a complete SquareRoot class:

// Includes
#include <cstdlib>
#include <iostream>
#include <math.h>
#include <vector>
#include <functional>

// Class with callback
class SquareRoot {
public:
  // Callback typedefs
  using TCallback = std::function<void(const size_t, const double)>;
  using TCallbackVector = std::vector<TCallback>;

  // Push callbacks onto the stack.
  void add_callback(TCallback cb) {
    m_callbacks.push_back(cb);
  }

  // Main logic of class.
  double run(const double input) {
    if (input < 0.0) throw 0; // Error checking.
    this->iteration = 0;      // Reset iteration number.
    double guess = input;     // Set initial guess to input.
    // Babylonian method.
    while (std::fabs(guess - input/guess) > this->epsilon) {
      for (const auto &cb : m_callbacks) {
        cb(iteration, guess);
      }
      guess = (guess + input / guess) / 2.0;
      ++iteration;
    }
    return guess;
  }

private:
  const double epsilon = 1e-6; // Maximum Allowed Error.
  size_t iteration = 0;        // Iteration Number.

  // Data member holding callbacks.
  TCallbackVector m_callbacks;

};

int main() {

  SquareRoot p;
  std::cout << "Result: " << p.run(1234.5*1234.5) << std::endl;

  return EXIT_SUCCESS;

}

At this point, we have added the structure necessary to allow the user to add callbacks; but we haven’t actually added any. So you’re welcome to compile and run this code again, but the output should be the same.

Defining the Callbacks

For this example, we will define four callbacks: one for each type of callable discussed above.

Function Pointer

To define a callback as a function pointer, we begin by defining a function matching the signature we used in TCallback:

void FunctionPointerCallback(const size_t iteration, const double guess) {
  std::cout << iteration << " : " << guess << " (Function Pointer)\n";
}

Moreover, since c++11, we can use auto to easily define a function pointer, and add it to our Program instance, p:

auto *cb_a = FunctionPointerCallback;
p.add_callback(cb_a);

By using auto, we have avoided the cumbersome function pointer syntax we would have needed to contend with prior to c++11.

Pointer to Member Function

While a function is simple and convenient, it is sometimes useful to have the flexibility of a full class instance (if, for example, you need to store data or organize functionality into multiple methods). Defining such a callback is as simple as defining a class (again, keeping in mind that the function signature of the callback method must match that of TCallback):

class MemberFunctionCallback {
public:
  void Call(const size_t iteration, const double guess) {
    std::cout << iteration << " : " << guess << " (Member Function)\n";
  }
};

Defining a pointer to a member function is simple using auto:

auto cb = &MemberFunctionCallback::Call;

However, this cannot be passed directly to add_callback. The reason for this can be understood by recognizing that, though the function signature of cb may appear to match TCallback, in actuality cb takes an invisible first argument (*this) to a particular instance of the class. Luckily, this can be dealt with painlessly by using std::bind, which allows you to “bind” arguments to a callable, effectively creating a new callable where those arguments are implied. In this case, we bind a pointer to the class instance to the member function pointer, and use std::placeholders::_1 to indicate that the remaining argument (the iteration index) will be supplied during the invocation. The result is a new callable whose function signature matches TCallback:

MemberFunctionCallback cb_b_tmp;
auto cb_b = std::bind(&MemberFunctionCallback::Call, // function
                      &cb_b_tmp,                     // First argument (*this)
                      std::placeholders::_1,         // 1st placeholder
                      std::placeholders::_2);        // 2nd placeholder
p.add_callback(cb_b);

Functor

Although using std::bind is an elegant and quite readable solution to the problem of using a member function pointer as a callback, even this small bit of added complexity can be avoided by converting the callback in the previous section to a functor: i.e., a class which overloads operator().

class FunctorCallback {
public:
  void operator()(const size_t iteration, const double guess) {
    std::cout << iteration << " : " << guess << " (Functor)\n";
  }
};

Since an instance of a functor is itself a callable (unlike a method pointer), it can be passed directly to add_callback:

FunctorCallback cb_c;
p.add_callback(cb_c);

Lambda Function

The last type of callable we’ll cover is the lambda function (introduced in c++11). Using auto, a lambda function instance can be easily captured and passed to add_callback:

auto cb_d = [](const size_t iteration, const double guess)
  { std::cout << iteration << " : " << guess << " (Lambda)\n"; };
p.add_callback(cb_d);

Putting it All Together

In this post, we’ve discussed a motivating problem which callbacks address; constructed a toy example for experimenting with their implementation; defined a simple callback mechanism; and shown four types of callables compatible with this mechanism. Here is the completed example, demonstrating how all the pieces we’ve discussed fit together.

// Includes
#include <cstdlib>
#include <iostream>
#include <math.h>
#include <vector>
#include <functional>

void FunctionPointerCallback(const size_t iteration, const double guess) {
  std::cout << iteration << " : " << guess << " (Function Pointer)\n";
}

class MemberFunctionCallback {
public:
  void Call(const size_t iteration, const double guess) {
    std::cout << iteration << " : " << guess << " (Member Function)\n";
  }
};

class FunctorCallback {
public:
  void operator()(const size_t iteration, const double guess) {
    std::cout << iteration << " : " << guess << " (Functor)\n";
  }
};

// Class with callback
class SquareRoot {
public:
  // Callback typedefs
  using TCallback = std::function<void(const size_t, const double)>;
  using TCallbackVector = std::vector<TCallback>;

  // Push callbacks onto the stack.
  void add_callback(TCallback cb) {
    m_callbacks.push_back(cb);
  }

  // Main logic of class.
  double run(const double input) {
    if (input < 0.0) throw 0; // Error checking.
    this->iteration = 0;      // Reset iteration number.
    double guess = input;     // Set initial guess to input.
    // Babylonian method.
    while (std::fabs(guess - input/guess) > this->epsilon) {
      for (const auto &cb : m_callbacks) {
        cb(iteration, guess);
      }
      guess = (guess + input / guess) / 2.0;
      ++iteration;
    }
    return guess;
  }

private:
  const double epsilon = 1e-6; // Maximum Allowed Error.
  size_t iteration = 0;        // Iteration Number.

  // Data member holding callbacks.
  TCallbackVector m_callbacks;

};

int main() {

  SquareRoot p;

  // Function Pointer
  auto *cb_a = FunctionPointerCallback;
  p.add_callback(cb_a);

  // Member Function
  MemberFunctionCallback cb_b_tmp;
  auto cb_b = std::bind(&MemberFunctionCallback::Call, // function
                        &cb_b_tmp,                     // First argument (*this)
                        std::placeholders::_1,         // 1st placeholder
                        std::placeholders::_2);        // 2nd placeholder
  p.add_callback(cb_b);

  // Functor
  FunctorCallback cb_c;
  p.add_callback(cb_c);

  // Lambda
  auto cb_d = [](const size_t iteration, const double guess)
    { std::cout << iteration << " : " << guess << " (Lambda)\n"; };
  p.add_callback(cb_d);

  std::cout << "Result: " << p.run(1234.5*1234.5) << std::endl;

  return EXIT_SUCCESS;

}

Callbacks are a striking example of how the improvements in c++11 can greatly simplify the syntax for important programming paradigms. I hope that this post provide a simple starting point for implementing callbacks in your own project, and perhaps that it will spark interest in exploring c++11 (and c++14, and c++17, and beyond!) in greater depth, to see how our lives as writers and readers of code can be made easier with modern c++. Happy coding! :-)