A callback is a function that you pass into another function as an argument. It is not executed right away; instead, the callback function is held and run later, often when some event or condition happens. This approach gives you more options and lets you create dynamic interactions in your programs.
Event handling is the process of responding to different actions or events in a program. These can be things users do, like clicking or typing, or events created by the system, such as a timer running out or data download completion. With event handling, the program waits for these events and then runs specific code, usually a callback function, in reaction. This is really important for making interactive and responsive applications. They work based on what the user does or what happens outside, rather than simply following the same steps each time. Callbacks and event handling are key parts of many new applications, enabling them to adapt and react based on current events.
Callbacks
One of the most common uses for function pointers is to pass a function as an argument to another function. Functions that you use as arguments are known as callback functions.
They let the program choose which function to call in response to a specific event or condition. This makes the code more adaptable and modular.
Here's how to use callback functions:
Step 1: Define the callback function. You start by defining the callback function's type.
Step 2: Create a function that accepts the callback. Next, make a function that takes a pointer to the callback function as a parameter and calls it.
Step 3: Implement the callback function. Then, make one or more functions that match the callback function’s definition.
Step 4: Use the callback function. At last, call the function with your callback function as an argument.
#include <iostream>
// Callback function type (step 1)
void (*callback)();
// Function that accepts a callback (step 2)
void doOperation(void (*callback)()) {
// ... performing some operations ...
// Calling the callback function
callback();
}
// Callback function (step 3)
void myCallbackFunction() {
std::cout << "Callback function is called." << std::endl;
}
// Main function
int main() {
doOperation(myCallbackFunction); // step 4
return 0;
}
When you run this code, the doOperation function calls myCallbackFunction as a callback. This way, you can easily change how doOperation works by using different callback functions.
Callbacks for sorting function
Let's look at this algorithm in a real example. Imagine you have a sorting function that can sort an array of numbers in different orders.
All sorting algorithms follow a similar pattern: the algorithm goes through a list of numbers, compares pairs, and swaps them based on the comparison results. By changing the comparison algorithm, you can change the way of sorting without touching other parts of the code.
Instead of writing many functions for each sort type, you can write one sorting function and give it a callback function. This defines how you want to compare the elements. Then you can break down the development into several steps:
Step 1: Defining the comparison function.
Step 2: Sorting function.
Step 3: Comparison functions.
Step 4: Using the sorting function.
Step 1. The comparison function takes two elements and returns true if the first element should come before the second in the sorted array, and false otherwise:
bool compare(int a, int b);Step 2. The sorting function accepts an array, its size, and a comparison function as arguments. It uses the comparison function to set the order of elements:
void sort(int* array, int size, bool (*compare)(int, int)) {
for (int i = 0; i < size - 1; i++) {
for (int j = 0; j < size - i - 1; j++) {
if (compare(array[j], array[j + 1])) {
// Exchange of elements
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}Notice that you can declare just a compare(int, int) function, but you need to pass a pointer (*compare)(int, int) as an argument.
Step 3. Now, you can create different comparison functions. For example, one for sorting in ascending order and another for descending order:
bool ascending(int a, int b) {
return a > b; // Sorting in ascending order
}
bool descending(int a, int b) {
return a < b; // Sorting in descending order
}Step 4. You can now use the sorting function with various comparison functions for different outcomes:
int main() {
int array[] = {5, 3, 2, 4, 1};
int size = sizeof(array) / sizeof(array[0]);
// Sorting in ascending order
sort(array, size, ascending);
// Sorting in descending order
sort(array, size, descending);
return 0;
}This example shows how callback functions make your code more adaptable, allowing you to define part of the function's behavior.
Events
Events are often used in event-driven programming. This is a way of writing programs where what happens in the program depends on events such as user clicks, messages from other programs, or internal alerts.
They are used a lot in:
Creating applications with a graphical user interface (GUI).
Server applications that handle incoming network requests.
Games, where what the user does directly affects what happens in the game.
In C++, you won’t find built-in support for event-driven programming as you would in JavaScript, Java, or Python. However, you can create this kind of behavior with callbacks — by using function pointers — or by utilizing different libraries. For example, Boost.Asio is good for asynchronous programming, and Qt helps with making GUI applications. These libraries have their own ways of managing events and callbacks.
Two main parts are needed to make events work:
Event handlers: these are functions or methods that get called when certain events happen.
Event loop: a key part of many event-driven systems. It waits for events and then calls the related handlers. When an event takes place, the event loop gets the right callback started.
Let's set up a simple system to handle events where we'll represent events with strings and event handlers with functions:
#include <iostream>
#include <map>
#include <vector>
#include <functional>
class EventManager {
public:
// Type for event handler
using EventHandler = std::function<void(const std::string&)>;
// Function to add an event handler
void subscribe(const std::string& event, EventHandler handler) {
handlers[event].push_back(handler);
}
// Function to generate events
void emit(const std::string& event, const std::string& data) {
if (handlers.find(event) != handlers.end()) {
for (auto& handler : handlers[event]) {
handler(data);
}
}
}
private:
std::map<std::string, std::vector<EventHandler>> handlers;
};
void handler1(const std::string& data) {
std::cout << "Handler 1: " << data << std::endl;
}
int main() {
EventManager manager;
// Subscribe to the event "test"
manager.subscribe("test", handler1);
// Subscribe to the event "test" using a lambda function
manager.subscribe("test", [](const std::string& data) {
std::cout << "Handler 2: " << data << std::endl;
});
// Generate the event "test"
manager.emit("test", "Hello, World!");
return 0;
}The result of this code will be:
Handler 1: Hello, World!
Handler 2: Hello, World!Remember, in our example, we sign up for an event and call it at one specific time and place, so it might seem simple. However, in a real application, different parts or even different gadgets would be involved. For example, you might have a button in a GUI. When you press it, certain methods run (like filling up a table, showing an image, or playing music). And it's not just different classes in the software that do this; different libraries might be involved, too.
In event-driven programming, you focus on reacting to events that come from outside or inside the program. It's not just about clicking on buttons. It could also be about getting a network message, a change in data, or a timer reaching a set time.
using and std::function
You may have noticed that we used 2 new keywords in the code: using and std::function. Let's take a closer look at why they are needed:
- using is utilized to create new names (aliases) for existing types. This can improve code readability and facilitate working with complex types. In our example, EventHandler is now an alias for std::function<void(const std::string&)>. This is convenient if such a data type is used in multiple places in the program. Generally, using has several contexts of use, but we use it for Type Aliasing.
- std::function in C++ is part of the standard library <functional> and represents a universal wrapper class. This class allows storing, calling, and managing callable objects, such as functions, lambda expressions, functors, and function pointers. More details can be found here.
Conclusion
Callbacks and events are key concepts for creating flexible and dynamic interactions in software. Callbacks let functions delegate certain actions or choices back to the caller, allowing for customized behavior. Events are vital in GUI programming or asynchronous operations, as they help respond to user actions or system events. Understanding these concepts is critical for making responsive and interactive apps.