Callbacks

You may have heard of callbacks, or even be using it without realizing, because they are referred to differently depending on the context. Delegate, event handler, function pointer, function object, functor, notifier, observer, predicate – are all callbacks.

What is a callback?
Why do we need callbacks?
How do you implement a callback?
  Example
  Function Pointer
    Ordinary function
    Class static member function
  Function Object
    Object instance
    Object non-static member function
  Lambda Function
  Interface Function
Simplifying code
    typedef
    using
    std::function
Conclusion

  • Instead of using caller and callee which are very similar sounding, client (caller) and library (callee) is used instead.
  • Raw syntax for pointer to function is used so that the underlying construct is clearly visible. However, in practice, this is error prone. Approaches to reduce such errors is discussed briefly in simplifying code.

What is a callback?

When a client calls a library, control is transferred to the library code. The client code gets control back only when the library function returns. However, what if you want to “temporarily” call back into the client before the library return? That’s the callback mechanism. The callback into the client code is a “temporary” transfer of control which then has to return to the library code. Eventually, the libarary code returns “permanently” to the client.

Why do you need callbacks?

  • Provide a custom predicate for algorithms in the standard library.
  • Extend some functionality with custom client behavior.
  • “Interrupt” the library execution to set some information to the client in that context.
  • “Interrupt” the library execution to get some information from the client in that context.
  • Notify the client of a certain state or condition, e.g. observer, event notification.

How do you implement a callback?

The library specifies the protocol, function prototype with parameters and return type it expects, since it is the one invoking the callback. The client follows the prototype and defines the implementation.

Example

The example code can be accessed on GitHub.
In this contrived example,

  • the library provides a val,
  • which the client acts on, in this case, multiplies by a factor, and
  • the product of val*factor has to be added to the client’s running total.

So one variable (val) belongs to the library and two variables (factor and total) belong to the client. Generally, since the library is independent of all clients, it defines a callback signature which the clients will implement to execute its operation(s).

Function Pointer

This is the legacy callback mechanism. A function is defined in the client and passed to the library.

Ordinary function

In its simplest form, the client defines a function with a library specified signature. This function is passed in as a parameter in one of the library calls.

To pass client state information, the library function has to be used as a pass through with extra parameters to send client data. This causes the library to define a signature which is dependent on the client, which is not recommended. So the simple function pointer as a callback, has a limitation when client internal state is required during a call back.

A workaround to manage the client state information (e.g. factor and total), is to resort to global variables. Ugly I know!. But this approach is used as a patch for existing software where the callback definition cannot be modified [1].

int g_total = 0;
int g_factor = 0;

// Define callback function pointer
void foo1(int val)
{
    g_total += g_factor * val;
}

void client()
{
    // Library call that accepts function pointer callback
    libFunctionPointer1(foo1);
}
void libFunctionPointer1(void(*cbFunction1)(int v))
{
    int val = 2;

    // Invoke callback via function pointer
    cbFunction1(val);
}

Class static member function

Instead of an ordinary global function, passing a static class member function is very similar. Pointers to static member functions are usually type compatible with regular pointers to functions [1]. This is also a better alternative to global variables to handle additional client data, as it remains in the scope of the class.

class SFoo
{
public:
    SFoo() {}

    // Define static member function callback
    static void sbar(int val)
    {
        msTotal += msFactor * val;
    }

public:
    static int  msTotal;
    static int  msFactor;
};
int SFoo::msTotal = 0;
int SFoo::msFactor = 0;

void client()
{
    // Library call that accepts function pointer callback
    libFunctionClasStaticMethod(SFoo::sbar);
}
void libFunctionClasStaticMethod(void(*cbSFunction)(int val))
{
    int val = 2;

    // Invokes callback via class static member function
    cbSFunction(val);
}

Function Object

Function object (sometimes called Functor) provides a mechanism to access client internal state/data during the callback. It is just an Object (instance of a Class) which defines a member function with the library specified signature. Here the necessary client internal data can be easily set as the object’s member data without impacting the library code. Since the Class name of the Object is not specified in the contract, it is generalized with a template argument in the library. Though this is quite elegant, it requires boiler plate code for the class definition even for simple operations.

// Define callback function object
class Foo
{
public:
    Foo(int& total, int factor) : 
    mrTotal(total),
    mFactor(factor) 
    {}
    
    // Non-static member function
    void bar(int val)
    {
        mrTotal += mFactor * val;
    }

    // Function call operator as member function
    void operator()(int val)
    {
        mrTotal += mFactor * val;
    }

    int& mrTotal;
    int  mFactor;
};

Object instance

Unlike a function pointer, the function object relies on the member function name, so it has to be established in the client/library contract. Typically, the member function call operator() with the prescribed parameters is the accepted protocol. When invoked in the calling library code, it looks just like a raw function call.

void client()
{
    int total = 0;
    int factor = 5;

    Foo foo(total, factor);

    // Library call that invokes function object callback
    libFunctionObject(foo);
}
template<class T>
void libFunctionObject(T& fo)
{
    int val = 3;

    // Invoke callback via function object
    fo.bar(val); // if name is the established contract
    fo(val);     // if function call operator is the established contract
}

Object non-static member function

When a member function, also called a method, cannot be established with a name, a pointer to the method can be used instead.

However, the type of pointer to member function is different from pointer to ordinary function.
Type is void(*)(int) if it’s an ordinary function.
Type is void(Foo::*)(int) if it’s a non-static member function of class Foo.
Furthermore, in C++, member functions have an implicit/hidden parameter which points to the object (the this pointer in the member function as the argument). These member functions can be invoked only by providing an object. For this reason, the library has to accept the object as an argument.

void client()
{
    int total = 0;
    int factor = 5;

    Foo foo(total, factor);

    // Library call that invokes function object method callback
    libFunctionObjectMethod(&Foo::bar, &foo);
}
template<class T>
void libFunctionObjectMethod(void(T::*cbMethod)(int v), T* pFO)
{
    int val = 3;

    // Invoke callback via function object
    (pFO->*cbMethod)(val);
}

Lambda Function

Introduced in C++11, a lambda function or lambda expression (lambda for short), combines the simplicity of a function pointer with the benefits of a function object. Lambdas facilitate definition of a callback function on the fly. That is why, they are sometimes referred to as anonymous or unnamed functions.

However, the vocabulary and syntax associated with it can be confusing. The lambda expression in the code generates a closure class during compilation, which in turn produces a closure for the runtime. The client state can be passed to the lambda with the capture semantics. See Lambda Functions in C++11 – the Definitive Guide for an overview.

void client()
{
    int total = 0;
    int factor = 5;

    // Library call that invokes lambda function callback
    libFunctionLambda(
        // Define callback anonymous lambda function
        [&total, factor] (int val) -> void
        {
            total = total + factor*val;
        }    
    );
}
void libFunctionLambda(std::function cbLambda)
{
    int val = 4;

    // Invoke callback via lambda function
    cbLambda(val);
}

Interface Function

This is the object oriented approach to a callback based on the properties of inheritance and polymorphism. It is very much like a function object, except that the callback method is explicitly specified in the interface definition and is enforced during compile time.

// Implement callback function interface
class FooImpl : public IFoo
{
public:
    FooImpl(int& total, int factor) : mrTotal(total), mFactor(factor) {}

    virtual void bar(int val) override
    {
        mrTotal = mrTotal + mFactor * val;
    }

private:
    int& mrTotal;
    int  mFactor;
};

void client()
{
    int total = 0;
    int factor = 5;

    FooImpl fooImpl(total, factor);

    // Library call the invokes interface callback
    libFunctionInterface(fooImpl);
}
// Define interface for callback
class IFoo
{
public:
    virtual void bar(int val) = 0;
};

void libFunctionInterface(IFoo& cbInterface)
{
    int val = 5;

    // Invoke callback via pre-defined interface
    cbInterface.bar(val);
}

Simplifying code

The syntax for function pointers are very confusing and easily prone to errors. A number of approaches are typically used to mitigate and manage this complexity.

typedef

A typedef provides a simple alias to the function pointer which makes it trivial to declare functions that receive it [2].

typedef void(*CallbackFunction)(int v);
void libFunctionPointer(CallbackFunction cbfn)
{
    int val = 2;
    cbfn(val);
}

using

C++11 introduced using which is sematically similar to typedef but makes it easier to read.

using CallbackFunction = void(*)(int v);
void libFunctionPointer(CallbackFunction cbfn)
{
    int val = 2;
    cbfn(val);
}

Furthermore, using is compatible with templates.

template<class T>
using CallbackObjectMethod = void(T::*)(int v);

template<class T> 
void libFunctionObjectMethod(CallbackObjectMethod cbMethod, T* pFO)
{
    int val = 3;
    (pFO->*cbMethod)(val);
}

std::function

C++11 provides a general purpose function wrapper std::function which operates at a higher level of abstraction.

void libFunctionGeneric(std::function cbGenericFunction)
{
    int val = 100;
    cbGenericFunction(val);
}
// No change calling with ordinary function
libFunctionGeneric(foofn);

// No change calling with class static member function
libFunctionGeneric(&SFoo::sbar);

// Calling with object non-static member function requires to bind the implicit "this" pointer.
Foo foo(total, factor);
libFunctionGeneric(std::bind(&Foo::bar, &foo, std::placeholders::_1));

// No change calling with lambda
libFunctionGeneric( [&total, factor](int val) -> void
                    {  ...  } ); 

Conclusion

The choice of which callback mechanism to use is subject to the problem at hand.

  • Function pointers are convenient for trivial cases, but lambdas makes them obsolete.
  • Function objects can be replaced by lambdas, but since lambdas are inline, many lines of code in the callback execution reduces readability. So, for a complex callback execution, function object is the better approach.
  • Everything a lambda can do, can be achieved by other means with a bit more typing, but it is a game changer for C++. So it will be the preferred approach and extensively used, with some exceptions.
  • Interface functions are unique with its ability to sub-class callback responses.

  1. isocpp: Pointers to Member Functions 
  2. isocpp: Typedef for pointers to Member Functions