Function pointers, functors and lambda functions

Programming Workshop 2 (CSCI 1061U)

Faisal Qureshi

Faculty of Science, UOIT

http://vclab.science.uoit.ca


Function Pointers

Consider the following file

square.cpp

#include <iostream>
using std::cout;
using std::endl;

int square(int x)
{
    return x*x;
}

int main()
{
    int (*fnptr)(int);
    fnptr = square;

    cout << fnptr(2) << endl;

    return 0;
}

When we compile and excute this file, we get the following

> g++ square.cpp -o square
> ./square
4

Here statement int (*fnptr)(int) declares a variable fnptr that can store a pointer to a function that takes one int argument and returns an int. In the above program square is such a function. Function square takes one int argument and returns an int. We can assign the address of this function to variable fnptr using the assignment fnptr = square. Note that the name of the function is also its address. So, we don't need to use the & to get the address of this function. We can then call the function square by using the variable fnptr.

Example 1

Consider the following function

void swap(float* a, float* b)
{
    float tmp;
    tmp = *a;
    *a = *b;
    *b = tmp;
}

We can declare a variable to store the pointer to function swap as follows

void (*m)(float* a, float* b);

We can then assign the address of function swap to variable m as follows

m = swap;

Example 2

Consider the following function

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

We can declare a variable to store the pointer to function gt as follows

bool (*l)(int a, int b);

We can then assign the address of function gt to variable l as follows

l =gt;

Example 3

Consider the following function

double sum(double a[], int i, int j, int s)
{
    double sum=0.0;
    for (int k=i; k<=j; k+=s) {
        sum += a[k];
    }
    return sum;
}

We can declare a variable to store the pointer to function sum as follows

double (*n)(double a[], int i, int j, int s)

We can then assign the address of function sum to variable n as follows

n = sum;

Function pointers as arguments

Having variables that can store pointers to functions allow us to pass functions as arguments to other functions. These is a very powerful feature that enables us to write code where processing can change on the fly.

Consider the following code. Here do_some_process is a function that relies upon the function passed to it as its argument to do the actually processing. This means that we can change the behavior of do_some_process by passing a different function at runtime.

function-ptr-arguments.cpp

#include <iostream>
using std::cout;
using std::endl;

int square(int x)
{
        return x*x;
}

int neg(int x)
{
    return -x;
}

int do_some_process(int (*fn)(int), int x)
{
    return fn(x);
}

int main()
{
    int x = 5;

    cout << do_some_process(square, x) << endl;
    cout << do_some_process(neg, x) << endl;

    return 0;
}

When we compile and run this code, we get the following

> g++ function-ptr-arguments.cpp -o prog
> ./prog
25
-5

Listener and callback functions

Function pointers are frequently used to set up listener or callback functions. These function are invoked when a certain event occurs. These provide an easy way for a user to inject their own functionality into a larger system. To reiterate this pointe, function pointers are often used to pass around processing instructions.

Below you see an example of a callback function for the GLUT library that allows a user to respond to mouse events.

void glutMouseFunc(void (*func)(int button, int state, int x, int y));

Virtual functions

It is also possible to avoid explicit function pointers by using virtual functions and polymorphism. Virtual function, however, are implemented behind the scene using function pointers.

C++ Functors

In addition to function pointers seen above, C++ also provides functors. Functors are objects that can be used as if these are functions. Functors are more powerful than good'ol function pointers, since functors can carry around state.

Consider the code below:

functors.cpp

#include <iostream>

using std::cout; 
using std::endl;

class Square {
    public:
    
    int operator()(int x) { 
        return x*x; 
    } 
};

int main() 
{
    Square a;

    cout << a(3) << endl;
    
    return 0; 
}

Square is a class. However, we have overloaded its () operator, which allows us to use it as if it is a function. In the above code instance a of Square behaves as if it is a function---a(3).

Using functors for callback

Functors are simply classes, and we can use templates to pass a functor as an argument.

#include <iostream>

using std::cout; 
using std::endl;

class Square {
    public:
    
    int operator()(int x) { 
        return x*x; 
    } 
};

class Neg { 
    public:
    
    int operator()(int x) { 
        return -x; 
    } 
};

template<typename T> 
int do_some_process(T process, int x)
{
    return process(x);
}

int main() 
{
    Square a;
    Neg b;

    cout << do_some_process(a, 2) << endl;
    cout << do_some_process(b, 2) << endl;
    
    return 0; 
}

An advantage of using the functors way of defining function arguments template<typename T> int do_some_process(T process, int x) { ... } is that it also allows us to pass in functions as below

#include <iostream>

using std::cout; 
using std::endl;

class Square {
    public:
    
    int operator()(int x) { 
        return x*x; 
    } 
};

class Neg { 
    public:
    
    int operator()(int x) { 
        return -x; 
    } 
};

int timestwo(int x)
{
    return 2*x;
}

template<typename T> 
int do_some_process(T process, int x)
{
    return process(x);
}

int main() 
{
    Square a;
    Neg b;
    int x = 9;

    cout << do_some_process(a, x) << endl;
    cout << do_some_process(b, x) << endl;
    cout << do_some_process(timestwo, x) << endl;
    
    return 0; 
}

Lambda functions (C++11)

C++11 standard introduces lambda functions. These are noname functions that can be defined and called at the same time. They are used extensively in some other languages, such as Javascript.

Consider the code below

#include <iostream>
using std::cout; 
using std::endl;

int main() 
{
    auto func = [](int x)->int{ return x*x; };
    
    int x = 11;
    cout << func(x) << endl;
    
    return 0; 
}

[](int x)->int{ return x*x; } defines the lambda function. Here [] is compiler specification, indicating the compiler that we are creating a lambda function. () is used to specify the argument list. In this case, this function takes one argument of type int, hence we use (int x). ->int indicates the return type. In this case the function returns an int. { return x*x; } is the function body. Here the lambda function is created and its pointer is assigned to variable func. auto is a keyword that can be used to define a variable. In this case the type of the variable is infered from context.

Syntactic sugar

It is okay to miss () if the function takes no arguments.

It is okay to miss -> if the compiler can discern the retunr type.

It is possible to call the function right away by using () at the end of the body as shown below

See the example below.

#include <iostream>
using std::cout; 
using std::endl;

int main() 
{
    int val = 11;
    cout << [](int x){ return x*x; }(val) << endl;
    
    return 0; 
}

Lambda functions as arguments

It is possible to pass lambda functions as arguments where function pointers are expected. Of course the signature of the lamda function should match the signature of the argument, representing the function pointer.

#include <iostream>
using std::cout; 
using std::endl;

int do_some_process(int x, int (*process)(int))
{
    return process(x); 
}

int main() 
{
    cout << do_some_process(2, [](int x)->int{ return x*x;} ) << endl; 
    return 0;
}

Variable capture

Consider the following program

#include <iostream>
using std::cout; 
using std::endl;

int main() 
{
    int val = 11;
    cout << [&](){ return val*val; }() << endl;
    
    return 0; 
}

Notice that the lambda function [&](){ return val*val; } is referring to a variable val, which is not defined in this lambda function. The variable val is also not passed into this lambda function. So how did this variable got here. We use the variable capture indicated by [&] to capture the variable val defined in the caller context to be available within the lambda function.

Notation Effect
[] Capture nothing
[&] Capture any variable referenced in the lambda function by reference
[=] Capture any variable referenced in the lambda function by value (i.e., making a copy)
[=,&foo] Capture any variable referenced in the lambda function by value (i.e., making a copy); capture variable foo by reference.
[this] Capture the this pointer of the enclosing class. This means that all members of the enclosing class are available within the lambda function
[foo] Capture variable foo by making a copy; do not capture anything else

Use care since lambda function can modify the values of the variables that are captured by reference. Also take care when returning lambda function from a function, since captured variables might become invalid.

Lambda functions and STL

Below we see an example of lambda function being used with the Standard Template Library (STL)

#include <iostream> 
#include <vector> 
using std::cout; 
using std::endl;

int main() 
{
    std::vector<int> v;
    for (int i=0; i<10; ++i) v.push_back(i*2);

    std::for_each(v.begin(), v.end(), [](int val){ cout << val << endl; });

    return 0; 
}

References