C++ Programming

A Brief Introduction into Function Pointer and Lambda Functions in C++

While writing the article about delegates in Unreal Engine, I realized that a basic understanding of function pointers and lambda functions might be helpful for beginners. It may seem confusing at first that in C++, a pointer to a function is used as a parameter and then passed as an argument. One of the keys to understanding is that in C/C++, you develop very close to the hardware. This allows you to reduce both variables and functions to their basic essence: essentially, they are areas of memory in RAM, each referenced by an address.

If the data type of the data structure lying in the memory area is known, it can be dereferenced and used. This applies to all data types like integers, floats, and chars, but also to functions. In this article, I explain how to handle function pointers, expect them as parameters for a function, and pass them as arguments when calling a function.

Function Pointers

A function pointer in C++ is a variable that stores the address of a function. This allows functions to be dynamically selected and called during runtime. For example, a function pointer can be passed as an argument to another function, allowing the called function to determine which function is actually executed.

This is best demonstrated with an example. For this purpose, we define two functions that can add and subtract numbers. Instead of calling the functions directly, we use a pointer to the function to perform the operation.

#include <iostream>

// Function for addition
int add(int x, int y) {
    return x + y;
}

// Function for subtraction
int subtract(int x, int y) {
    return x - y;
}

So far, nothing extraordinary. I have defined two functions, add and subtract, which both return an integer and expect two integers as parameters. Now I define a function that expects and executes a function with exactly this signature as a parameter:

// Function that takes a function pointer and two int values, and performs the operation
int performOperation(int (*operation)(int, int), int a, int b) {
    return operation(a, b);
}

The first parameter of performOperation is the function pointer described above. To clarify that you can choose the name of the function freely, I called it operation. You can choose a more descriptive name for it. The syntax of the parameter indicates that the passed function pointer must have an integer as a return value and expect two integers as parameters. To the function performOperation, we also pass two integers, a and b, which are then passed to operation as arguments.

Here's how it looks in action:

int main() {
    int a = 5;
    int b = 3;

    // Calling performOperation with the pointer to the addition function
    std::cout << "Addition: " << performOperation(add, a, b) << std::endl; // Outputs 8

    // Calling performOperation with the pointer to the subtraction function
    std::cout << "Subtraction: " << performOperation(subtract, a, b) << std::endl; // Outputs 2

    return 0;
}

We call the function performOperation and pass the function as the first argument, followed by the two integer variables to be added or subtracted from each other.

A good illustrative example would be a sorting function from the Standard Template Library. Suppose you want to implement a sorting function for your inventory system so that your items are sorted first by category and then by name. Armor should be at the top, followed by weapons, and then consumables. You pass a list of objects to the std::sort function and want them to be sorted as efficiently as possible. But how is the function supposed to know how you want your items sorted when you call it?

In C++, we can use function pointers for such scenarios. This is especially useful when we want functions like the comparison function in std::sort to be interchangeable and definable at runtime.

Let's assume we have a simple Item structure and a list of Item objects. We want to sort this list based on various criteria. Here's how we could do it:

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

struct Item {
    std::string category;
    std::string name;
};

// Comparison function
bool compareItems(const Item &a, const Item &b) {
    if (a.category != b.category) {
        return a.category < b.category;
    } else {
        return a.name < b.name;
    }
}

int main() {
    std::vector<Item> inventory = {
        {"Armor", "Plate Armor"},
        {"Weapon", "Sword"},
        {"Consumable", "Healing Potion"},
        {"Armor", "Leather Vest"},
        {"Weapon", "Bow"}
    };

    // Sorting with the comparison function
    std::sort(inventory.begin(), inventory.end(), compareItems);

    // Output of the sorted inventory
    for (const auto &item : inventory) {
        std::cout << item.category << ": " + item.name << std::endl;
    }

    return 0;
}

In this example:

  1. We define an Item structure with a category and a name.
  2. We implement a comparison function compareItems, which determines how two Item objects are compared. This function will be passed to std::sort later.
  3. We use std::sort to sort the inventory. Here, we directly pass our compareItems function as an argument to the function as a variable.

This code snippet shows how to use a custom comparison function with std::sort to sort a vector of Item structures first by category, then by name. The compareItems function checks if the categories are different and sorts based on that first. If the categories are the same, it then sorts by the item names.

Thus, we can pass a function as a variable to another function when calling it as an argument, which can then be used by that function to sort items in the inventory system, as shown in this example.

Pointers to Member Functions

Having familiarized ourselves with the basics of function pointers, it's time to take a step further and look into pointers to member functions. These are a bit more complex in C++ as they refer not just to a function, but are used in the context of an object.

A pointer to a member function is a pointer that points to a function within a class. Unlike regular function pointers, they must be used in the context of an object, as member functions access the data of a specific object.

Syntax and Usage

The syntax for declaring a pointer to a member function is somewhat different than that of a regular function pointer. Here is an example:

class MyClass {
public:
    void myMemberFunction(int x) {
        std::cout << "Value: " << x << std::endl;
    }
};

int main() {
    // Declaration of a pointer to a member function of MyClass
    void (MyClass::*pointerToMember)(int) = &MyClass::myMemberFunction;

    MyClass myObject;

    // Calling the member function via the pointer
    (myObject.*pointerToMember)(10); // Calls myMemberFunction for myObject

    return 0;
}

In this example, we have a class MyClass with a member function myMemberFunction. The pointer pointerToMember is declared to point to a member function of MyClass that takes an int as a parameter. Then, we create an object myObject of the class MyClass and call myMemberFunction using the pointer pointerToMember.

Special Features:

  • Context Dependency: Unlike regular function pointers, a pointer to a member function is always context-dependent. This means it must always be used in conjunction with an object of the corresponding class.
  • Usage with this Pointer: In member functions, pointers to other member functions of the same class can be used along with the this pointer to refer to the member functions of the current object.
  • Usage with Pointers and References: You can also use pointers to member functions with object pointers or references. When used with an object pointer, replace the operator .* with ->*.

To clarify the differences and similarities between function pointers and pointers to member functions, let's revisit the first example and modify it so that a pointer to member functions is expected:

First, let's define a class with the member functions add and subtract:

#include <iostream>

class MathOperations {
public:
    int add(int x, int y) {
        return x + y;
    }

    int subtract(int x, int y) {
        return x - y;
    }
};

Now, we define a function that takes a pointer to a member function and two integers as arguments and performs this operation on an object of the MathOperations class:

// Function that takes a pointer to a member function of MathOperations, a MathOperations object
// and two int values, and performs the operation
int performOperation(MathOperations& obj, int (MathOperations::*operation)(int, int), int a, int b) {
    return (obj.*operation)(a, b);
}

Finally, we use this structure in our main function:

int main() {
    MathOperations mathObj;
    int a = 5;
    int b = 3;

    // Calling the member function via the pointer
    std::cout << "Addition: " << performOperation(mathObj, &MathOperations::add, a, b) << std::endl; // Outputs 8

    // Calling the member function via the pointer
    std::cout << "Subtraction: " << performOperation(mathObj, &MathOperations::subtract, a, b) << std::endl; // Outputs 2

    return 0;
}

In this example, the performOperation function is used to execute an operation (either addition or subtraction) on a MathOperations object. The specific operation is specified by a pointer to a member function of this class.

Unlike regular function pointers, which are independent of objects, pointers to member functions require an object on which they operate. In this example, mathObj is the object on which the operations are performed. The pointers addPointer and subtractPointer point to the add and subtract member functions of the MathOperations class, respectively. The performOperation function takes a MathOperations object, a pointer to a member function, and two integer values as arguments and executes the corresponding operation.

Pointers to Static Member Functions

Dealing with pointers to static member functions is somewhat simpler than with non-static member functions, as static member functions do not have access to instance-specific data of an object and therefore do not require an object context.

First, let's define a class with static member functions for addition and subtraction:

#include <iostream>

class MathOperations {
public:
    static int add(int x, int y) {
        return x + y;
    }

    static int subtract(int x, int y) {
        return x - y;
    }
};

Since the functions are static, we can call them without creating an instance of the class. We now define a function that takes a pointer to a static member function and two integers as arguments:

// Function that takes a pointer to a static member function of MathOperations
// and two int values, and performs the operation
int performOperation(int (*operation)(int, int), int a, int b) {
    return operation(a, b);
}

In the main function, we use this structure:

int main() {
    int a = 5;
    int b = 3;
  
    // Calling the static member function via the pointer
    std::cout << "Addition: " << performOperation(MathOperations::add, a, b) << std::endl; // Outputs 8

    // Calling the static member function via the pointer
    std::cout << "Subtraktion: " << performOperation(MathOperations::subtract, a, b) << std::endl; // Outputs 2

    return 0;
}

In this example, addPointer and subtractPointer are pointers to static member functions of the MathOperations class. The performOperation function takes a pointer to a static member function and two integer values as arguments and executes the corresponding operation.

The main difference compared to non-static member functions is that static member functions can be called independently of specific instances of the class. Therefore, it is possible to use a pointer to a static member function, similar to regular function pointers. They do not need an object to be called and can thus be treated like global functions.

Lambda Functions

After exploring how function pointers and pointers to member functions can be used, it's now time to delve into a more modern approach: Lambda Functions.

Lambda functions in C++ are a type of anonymous functions that can be defined directly in the code. Introduced in C++11, they offer a convenient and compact way to write small functions that are often used only once, such as for short callbacks or functions passed to algorithms.

A lambda function in C++ looks like this:

[ capture clause ] ( parameters ) -> return_type { body }
  • Capture Clause: Determines how and which variables from the surrounding scope are captured for use in the lambda function. For example, [=] captures all local variables by value, and [&] captures them by reference.
  • Parameters: Similar to regular functions, these are the parameters passed to the lambda function.
  • Return Type: Optional. The function's return type. If omitted, it is inferred by the compiler.
  • Body: The code executed when the lambda function is called.

Here is a simple example demonstrating the use of lambda function in C++:

#include <iostream>
#include <functional> // For std::function

// Function that takes a std::function and two integers and performs the operation
int performOperation(std::function<int(int, int)> operation, int a, int b) {
    return operation(a, b);
}

int main() {
    int a = 5;
    int b = 3;

    // Lambda function for addition
    auto add = [](int x, int y) -> int {
        return x + y;
    };

    // Lambda function for subtraction
    auto subtract = [](int x, int y) -> int {
        return x - y;
    };

    std::cout << "Addition: " << performOperation(add, a, b) << std::endl; // Outputs 8
    std::cout << "Subtraction: " << performOperation(subtract, a, b) << std::endl; // Outputs 2

    return 0;
}

Explanation:

  1. The performOperation function is a regular function that takes a std::function<int(int, int)> and two int values as parameters and executes the passed function.
  2. We still define lambda functions for add and subtract.
  3. performOperation is called, once with the add lambda function and once with the subtract lambda function, outputting the results.

Lambda functions in C++ provide a flexible and compact way to define functions, especially for short-lived operations or operations needed only in one place in the code. They are particularly useful in combination with standard library algorithms, callbacks, or situations where you want to pass a function as an argument to another function.

To illustrate how lambda functions can be effectively used with the standard library, here's the sorting example reimplemented with a lambda function:

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

struct Item {
    std::string category;
    std::string name;
};

int main() {
    std::vector<Item> inventory = {
        {"Armor", "Plate Armor"},
        {"Weapon", "Sword"},
        {"Consumable", "Healing Potion"},
        {"Armor", "Leather Vest"},
        {"Weapon", "Bow"}
    };

    // Sorting with a lambda function
    std::sort(inventory.begin(), inventory.end(), 
        [](const Item &a, const Item &b) -> bool {
            return a.category < b.category || 
                   (a.category == b.category && a.name < b.name);
        }
    );

    // Output of the sorted inventory
    for (const auto &item : inventory) {
        std::cout << item.category << ": " + item.name << std::endl;
    }

    return 0;
}

This example demonstrates the use of a lambda function in conjunction with the std::sort function from the C++ Standard Library to sort a list of objects. Here's a more detailed explanation:

  1. Structuring and Data: First, a Item structure is defined with two properties: category and name. A std::vector of Item objects named inventory is created, containing various items.
  2. Lambda Function in std::sort: To sort the list, the std::sort function is used. A lambda function is passed as the third argument. This lambda function defines the sorting logic: It takes two Item objects (a and b) and compares them.
  3. Sorting Logic: The sorting is done in two steps. First, it sorts by category (category). If the categories are identical, the name (name) is used as the second criterion. The lambda function returns true if a should be sorted before b, which is determined by the < operation.
  4. Output of the Sorted Inventory: After the sorting is completed, the sorted inventory is output. A loop iterates through all elements of the inventory vector and outputs them in sorted order.

This example illustrates the effective use of lambda functions in C++, especially in combination with standard library functions like std::sort. Lambda functions provide a convenient and clear way to bring user-defined logic to the place of its use, which is particularly beneficial in algorithms such as sorting. This makes the code more compact and improves its readability, as the sorting logic is defined close to its application and not as a separate function or function pointer.

Profile picture

Artur Schütz

Senior Software Developer specializing in C++ graphics and game programming. With a background as a Full Stack Developer, Artur has now shifted his focus entirely to game development. In addition to his primary work, Artur explores the field of Machine Learning, experimenting with ways to enrich gameplay through its integration.