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:
- We define an
Item
structure with acategory
and aname
. - We implement a comparison function
compareItems
, which determines how twoItem
objects are compared. This function will be passed tostd::sort
later. - We use
std::sort
to sort the inventory. Here, we directly pass ourcompareItems
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 thethis
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:
- The
performOperation
function is a regular function that takes astd::function<int(int, int)>
and twoint
values as parameters and executes the passed function. - We still define lambda functions for
add
andsubtract
. performOperation
is called, once with theadd
lambda function and once with thesubtract
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:
- Structuring and Data: First, a
Item
structure is defined with two properties:category
andname
. Astd::vector
ofItem
objects namedinventory
is created, containing various items. - Lambda Function in
std::sort
: To sort the list, thestd::sort
function is used. A lambda function is passed as the third argument. This lambda function defines the sorting logic: It takes twoItem
objects (a
andb
) and compares them. - 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 returnstrue
ifa
should be sorted beforeb
, which is determined by the<
operation. - 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.