Skip to content

Pointer to function, function types, lambdas and std::function

C++ offer great flexibility regarding how to declare and use functions.

It inherits all that plain C can do and offers nice, extra features in the language and in the std lib.

Pointers

pointers can store addresses, and functions has addresses as well. Therefore, this is valid C++ code:

cpp

#ifndef POINTER_TO_FUNCTION_CC
#define POINTER_TO_FUNCTION_CC

#include <iostream>

namespace ptf
{

  void duplicator(int &n)
  {
    n *= 2;
  }

  void incrementor(int &n)
  {
    n += 1;
  }

  void applier(int *v, int vSize, void (*f)(int &))
  {
    for (int i = 0; i < vSize; i++)
    {
      f(v[i]);
    }
  }

  void printer(int *v, int vSize)
  {
    for (int i = 0; i < vSize; i++)
    {
      std::cout << v[i] << " ";
    }
    std::cout << std::endl;
  }

};

#endif // POINTER_TO_FUNCTION_CC

The void (*f)(int &) might look like a bit clumsy, but the parenthesis around *f make sure that you ton't mistype the return type (for example int* instead of int).

Function typedefs

Optionally, you can write a typedef to make it easier to the eye:

cpp
#ifndef FUNCTION_TYPEDEF_CC
#define FUNCTION_TYPEDEF_CC

namespace ptf
{
  typedef void (*func_t)(int &);
};

#endif // FUNCTION_TYPEDEF_CC

It's possible now to write something like ptf::func_t f = ptf::duplicator.

Lambdas

Pass functions around for later use deciding on the context is nice, however the context associated with it is limited.

For instance, you are constrained by the function contract (how many parameters, their types and return type) and this is all you get available when the time comes.

This is also why the opaque pointer parameter is ofter present in libraries consuming pointer to functions.

On the other hand, lambdas can carry references and values from the context they came from:

cpp
void main()
{
  std::cout << "lambda" << std::endl;
  int x = 0;
  auto sample = [&x](int &n)
  {
    n -= 1;
    x += n;
  };
  ptf::applier2(v, 5, sample);
}

Lambda has these key parts:

  • type: the variable type for this lambda. auto is used in this specific example, but there are other possibilities.
  • name: variable name, how you will refer to this lambda. it is sample in this example.
  • context capture: this is where lambda shines. set the shared context between the current function and the lambda being declared. it is the [&x]. You can define the capture in many ways, by value, by reference, pass nothing at all, you call it.
  • parameter list: like a regular function, the parameter list the lambda will receive when it gets called. it' the (int &n) part.
  • lambda's body: function body to be executed.

It might look ugly or too similar to a regular function, but in fact it has more similarities with classes than with functions.

Thanks to the capture group, lambdas can carry state.

std::function

A side effect of this is that lambda signatures, when have a non-empty capture group, doesn't fit well in regular function pointers, leading to unexpected behavior and compile errors.

To solve that, functions consuming lambdas must adapt:

cpp

#include <iostream>
#include <functional>

namespace ptf
{
  void applier2(int *v, int vSize, std::function<void(int &)> f)
  {
    for (int i = 0; i < vSize; i++)
    {
      f(v[i]);
    }
  }
};

You can check the usage of each approach in the main.cc:

cpp

#include <iostream>

#include "pointer-to-function.cc"
#include "function-typedef.cc"
#include "std-function-printer.cc"

int main(int argc, char *argv[])
{
  // our sample data
  int v[] = {1, 2, 3, 4, 5};
  // pointer to function
  std::cout << "pointer to function:" << std::endl;
  ptf::applier(v, 5, ptf::duplicator);
  ptf::applier(v, 5, ptf::incrementor);
  ptf::printer(v, 5);
  std::cout << std::endl;

  // function typedefs
  std::cout << "function typedef:" << std::endl;
  ptf::func_t f[] = {ptf::duplicator, ptf::incrementor};
  for (int i = 0; i < 2; i++)
  {
    ptf::applier(v, 5, f[i]);
  }
  ptf::printer(v, 5);
  std::cout << std::endl;

  // lambdas
  std::cout << "lambda" << std::endl;
  int x = 0;
  auto sample = [&x](int &n)
  {
    n -= 1;
    x += n;
  };
  ptf::applier2(v, 5, sample);
  ptf::printer(v, 5);
  std::cout << "x = " << x << std::endl;
  return 0;
}

How to run

Simply compile and run the entrypoint:

bash
g++ -Wall main.cc -o main
./main

Noteworthy

  • Technically speaking, class methods and lambdas are equivalent, except that lambdas are a lightweight syntax to get the same result: custom context for a function.
  • The example above includes source files instead of headers and also compiles one single entrypoint. It's called jubmo build.