The power and the pain of function pointers are fast and C-compatible but have clunky syntax, can't hold state, and require unsafe void* tricks to pass user data.
Modern C++ provides a beautiful solution to all these problems with std::function. It is a general-purpose, polymorphic function wrapper available in the <functional> header.
Think of std::function as a "smart" function pointer. It can hold, copy, and invoke any callable object. Not just function pointers, but also lambdas.
The problem with raw pointers is that a void (*)(int) is a completely different, incompatible type from a void (*)(double). std::function solves this by "erasing" the specific type of the callable it holds, storing it in a generic, type-safe wrapper.
The syntax is clean and descriptive:
std::function<return_type(parameter_types)>
#include <iostream>
#include <functional>
#include <string>
// A regular function
void printMessage(const std::string& msg) {
std::cout << "Regular function: " << msg << std::endl;
}
// A function object (functor)
struct Divider {
double operator()(int a, int b) {
return static_cast<double>(a) / b;
}
};
int main() {
// 1. Declare a std::function that can hold any callable
// taking a const string& and returning void.
std::function<void(const std::string&)> p_print;
// Assign a regular function to it
p_print = printMessage;
p_print("Hello World!"); // Call it like a normal function
// 2. Declare a std::function for a different signature
std::function<double(int, int)> p_calculate;
// Assign a lambda to it
p_calculate = [](int x, int y) { return x * y; };
std::cout << "Lambda result: " << p_calculate(10, 5) << std::endl;
// Assign a function object (functor) to it
Divider divider;
p_calculate = divider;
std::cout << "Functor result: " << p_calculate(10, 4) << std::endl;
}Notice how the same p_calculate object first held a lambda and then a functor. This flexibility is the superpower of std::function.
Remember the unsafe void* trick we used to pass user data to a C-style callback? Lambdas and std::function make that pattern obsolete.
A lambda can capture variables from its enclosing scope. When you store that lambda in a std::function, the captured state is carried along with it, safely and transparently.
Let's refactor our old void* example into modern, safe C++.
void on_button_click(void* data) {
UserData* userData = static_cast<UserData*>(data); // Unsafe cast!
userData->clickCount++;
}#include <functional>
struct UserData {
std::string name;
int clickCount = 0;
};
// This function now takes a std::function. No more void*!
void register_callback(std::function<void()> callback) {
// Imagine an event happens... now we just call the function.
// We don't need to know anything about user data.
std::cout << "Event occurred, executing callback..." << std::endl;
callback();
}
int main() {
UserData myData;
myData.name = "Alex";
// We create a lambda that captures `myData` by reference.
// The lambda "remembers" myData.
auto my_callback = [&myData]() {
myData.clickCount++;
std::cout << myData.name << " has now clicked "
<< myData.clickCount << " times." << std::endl;
};
// Pass the lambda to the function.
// The state (myData) is carried with it, 100% type-safely.
register_callback(my_callback);
register_callback(my_callback);
}No casts, no void*, no danger. The lambda's capture list [&myData] handles everything. This is the idiomatic way to handle stateful callbacks in modern C++.
std::function also massively simplifies calling member functions. You no longer need the awkward .* or ->* syntax. You can use a lambda to capture the object instance (this).
class Wallet {
public:
void addMoney(int amount) {
balance += amount;
std::cout << "Added " << amount << ", new balance is " << balance << std::endl;
}
private:
int balance = 0;
};
int main() {
Wallet myWallet;
// Create a std::function that takes an int and returns void
std::function<void(int)> p_operation;
// Assign a lambda that captures `myWallet` by reference and calls its member function.
p_operation = [&myWallet](int value){
myWallet.addMoney(value);
};
// Now just call it like a regular function
p_operation(50); // Calls myWallet.addMoney(50)
p_operation(20); // Calls myWallet.addMoney(20)
}- Type-Safe: Completely eliminates
void*casting errors for user data. This is its biggest win. - Flexible: Can hold any kind of callable object with a matching signature.
- Clean Syntax: Much easier to read and write than complex function pointer syntax.
- Slightly Slower Call: Calling through a
std::functioninvolves an extra layer of indirection (like a virtual function call), which can be marginally slower than a direct function pointer call.
Here's the modern C++ guidance:
- Default to Lambdas and
std::function: For any new C++ code involving callbacks, stored operations, or strategies, start here. The safety, flexibility, and clarity are almost always worth the minor potential overhead. - Use Function Pointers for C Interoperability: When you are writing a function that will be called by a C library, or you are calling a C function that expects a callback, you must use a function pointer.