In programming, a callable is anything that can be invoked or called as if it were a function.
C and C++ provide several mechanisms to create callables: from basic function pointers in C
to advanced features in C++ like functors (function objects), lambda expressions,
and utility wrappers (std::bind
, std::mem_fn
). These tools enable flexible patterns like
callback functions and inversion of control in software design.
Below are comprehensive notes covering each of these topics, with examples, real-world applications,
and interactive quizzes/exercises for practice.
Definition: A function pointer is a variable that stores the address of a function in memory. In other words, it “points” to executable code instead of data. Dereferencing and calling a function pointer will invoke the function it points to, making an indirect function call.
Syntax (C and C++):
For a function returning R
and taking parameters of types T1, T2, ...
, a pointer is declared as:
R (*ptrName)(T1, T2, ...);
For example, int (*fp)(double)
declares fp
as a pointer to a function that takes a double
and returns an int
. You can assign it to a matching function by name (the function decays to a pointer),
or explicitly use &
(e.g., fp = &someFunction;
).
To call the function via the pointer, use fp(args...)
(the *
dereference is optional when calling).
Example: Using a function pointer to choose a computation at runtime:
// Two simple functions with the same signature:
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
// A function that takes a function pointer as parameter
int compute(int x, int y, int (*op)(int, int)) {
// Call the passed-in function
return op(x, y);
}
int main() {
// Assign function pointer and call compute with different operations
int (*funcPtr)(int, int);
funcPtr = add;
printf("4 + 5 = %d\n", compute(4, 5, funcPtr));
funcPtr = multiply;
printf("4 * 5 = %d\n", compute(4, 5, funcPtr));
return 0;
}
Output:
4 + 5 = 9
4 * 5 = 20
In this example, compute
can call different functions (add or multiply) via the function pointer parameter op
.
This shows how function pointers enable dynamic selection of code.
Real-World Applications: Function pointers are used in C for callbacks and flexible APIs.
Classic examples include the C standard library qsort
function, which accepts a user-provided comparison
function to sort elements, and signal handlers (where signal()
takes a pointer to a handler function).
They are also used to implement state machines (arrays of function pointers), and in systems where you
need to pass a function as data (e.g., event handlers). In C++, function pointers can be used
similarly, though modern C++ often prefers higher-level callables (functors, lambdas). Function pointers remain useful
for interoperability with C libraries and for simplicity in small callbacks.
Quiz: Which of the following correctly declares a pointer to a function that takes two int
arguments and returns a double
?
What is a callback? In computer programming, a callback function is a function that is passed as an argument to another function so that it can be called later by that other function. In C and C++, callbacks are typically implemented using function pointers: you give a function pointer to some API, and the API “calls back” your function at the appropriate time. This mechanism is key to asynchronous operations and event-driven programming, where lower-level code invokes higher-level code on certain events.
How it works: The program supplies a pointer to a callback function when calling into another function or registering an event. The callee stores this pointer and calls the function when needed. This reverses the usual control flow: instead of your code calling a library function to check for events, your code is called by the library when an event occurs. (This is a simple case of inversion of control, discussed later.)
Analogy – “Calling a doctor”: Imagine you call a doctor’s office, but the doctor is busy. You leave your phone number with the receptionist – this is like passing a function pointer as a callback. Later, the doctor calls you back when ready – this is like the system invoking your callback function when the result or event is ready. You don’t wait on the line; instead, you continue with other tasks and get a “callback” when the doctor (the callee) is ready to respond.
Use Cases: Callbacks are widely used for:
qsort
requires a comparison callback to decide element order.
Similarly, in C++ you might pass a callback to a sort or search routine to customize its behavior.Example – Event callback in C++:
The following C++ program demonstrates a simple callback mechanism.
We define a callback function printResult
and a
function processArray
that takes a pointer to a callback.
The processArray
function applies the callback to each element
of an array (simulating an event or processing hook).
#include <iostream>
void printResult(int x) {
std::cout << "Processed value: " << x << std::endl;
}
// Function that takes an array and a callback function to apply to each element
void processArray(const int* arr, int length, void (*callback)(int)) {
for (int i = 0; i < length; ++i) {
// invoke callback on each element
callback(arr[i]);
}
}
int main() {
int data[] = {1, 2, 3};
// Call processArray with printResult as the callback
processArray(data, 3, printResult);
return 0;
}
Output:
Processed value: 1
Processed value: 2
Processed value: 3
Here, processArray
doesn’t know what printResult
does; it simply calls whatever function is passed to it. We could pass a different function (or even a lambda expression) to achieve a different behavior, illustrating the flexibility of callbacks in C++.
Another classic example of a callback in C++ is the use of standard library functions like std::sort
. You provide a comparison function (via a function pointer or a lambda) to std::sort
, and it will call back your comparison function repeatedly to determine the order of elements. In this scenario, your code (the comparison logic) is called by the library’s sorting algorithm – a hallmark of callback-based design.
Quiz: Which statement is TRUE about callback functions?
Exercise: Implement a function applyToEach
in C++ that takes an array of integers, its length, and a callback function, and applies the callback to each element. For example, applyToEach(arr, n, callback)
should call callback(value)
for each element in arr
. Write the function signature and a possible implementation.
Answer:
void applyToEach(int *arr, int length, void (*callback)(int)) {
for(int i = 0; i < length; ++i) {
callback(arr[i]);
}
}
What is a functor? In C++, a function object
(often called a functor) is an object that can be called as if it were a function.
This is achieved by giving the object a operator()
method.
When you use the object with parentheses like obj()
or obj(arg)
,
it invokes the operator()
method. Any class or struct that overloads operator()
becomes a callable.
Functors are defined in C++ only (there is no direct equivalent in C).
They are a cornerstone of the C++ standard library’s design.
Many algorithms (like those in <algorithm>
) can accept functors
to customize behavior. A key benefit of functors is that since they are objects,
they can hold state between calls. This makes them more powerful than
plain function pointers in many scenarios (for example, a functor could count how many times
it’s been invoked, or carry configuration data to use during calls).
Making a class callable: To create a functor, define a class/struct and overload the operator()
in it:
struct MyFunctor {
// Overload the function call operator
void operator()(int x) const {
// ... do something with x
}
};
MyFunctor f;
f(42); // This calls MyFunctor::operator()(42)
By overloading operator()
, we make MyFunctor
behave like a function.
Note that operator()
can be overloaded to accept parameters and return values like any function.
Marking it const
is common if it doesn’t modify the object’s state.
Example: A functor with internal state. Below, we create a functor that keeps a running count of how many numbers it has processed, demonstrating how state can be preserved across calls:
#include <iostream>
using namespace std;
struct CounterFunctor {
int count;
CounterFunctor(): count(0) {} // initialize count
void operator()(int x) {
++count;
cout << "Processing " << x
<< " (call #" << count << ")\n";
}
};
int main() {
CounterFunctor counter;
counter(10);
counter(20);
counter(30);
return 0;
}
Output:
Processing 10 (call #1)
Processing 20 (call #2)
Processing 30 (call #3)
The CounterFunctor
object counter
retains the count
field between calls. Each time counter(x)
is invoked, the count increases.
This is something not directly possible with a bare function pointer (which has no associated
storage for such state, aside from global or static variables).
Real-World Applications: Functors are used extensively in generic programming
and the Standard Template Library (STL).
For example, the standard library provides functors like std::less<T>
or std::greater<T>
for comparisons, which you can pass to sorting algorithms. Prior to C++11 (which introduced lambdas),
functors were the primary way to pass custom operations to STL algorithms
(std::for_each
, std::transform
, etc.). In event-driven systems or game development,
you might create functor objects that carry configuration and act as callbacks
(e.g., an event handler object that encapsulates its target widget).
Functors can also be used to implement strategy patterns or to store callbacks in data structures
(since they have a type and can be copied or stored, whereas function pointers are limited in type).
Modern C++ often uses lambdas for brevity, but those lambdas are themselves implemented as functor
objects under the hood.
Quiz: What enables a C++ object to be called like a function?
Exercise: Write a functor (struct) called Adder
that is constructed with an integer N
, and overload operator()
so that calling the functor with an int argument returns the sum of that argument and N
. For example, Adder add5(5); add5(10)
should return 15.
Answer:
struct Adder {
int N;
Adder(int n) : N(n) {}
int operator()(int x) const {
return x + N;
}
};
std::bind
and std::mem_fn
C++ provides utility templates in <functional>
to adapt and bind callables.
Two important ones are std::bind
and std::mem_fn
.
std::bind
std::bind
allows you to create a new function (technically, a function object) by binding some or all arguments of an existing function or callable. It can also reorder arguments. In essence, std::bind
generates a call wrapper for a callable f
, where some arguments are “bound” to fixed values.
The resulting object can be called with the remaining parameters.
Syntax: auto boundFunc = std::bind(f, arg1, arg2, ..., std::placeholders::_1, ...);
Here, f
is the target function (or functor), and you provide a mix of actual arguments or std::placeholders::_n
tokens. The _1, _2, ...
placeholders represent arguments that will be supplied later when the bound function is called. You can bind some arguments to specific values, or even bind none (which effectively copies the function into a functor with the same signature).
Example 1: Partial application. Suppose we have a function int multiply(int a, int b)
. We can use std::bind
to create a new callable that always multiplies by 2:
#include <functional>
#include <iostream>
#include <algorithm>
using namespace std::placeholders; // for _1, _2, ...
int multiply(int a, int b) {
return a * b;
}
int main() {
// Bind the first argument to 2, leave second argument as a placeholder
auto timesTwo = std::bind(multiply, 2, _1);
std::cout << "5 * 2 = " << timesTwo(5) << std::endl;
return 0;
}
Output:
5 * 2 = 10
We created timesTwo
as a new function object that takes one argument (the second operand). When called, it actually invokes multiply(2, x)
using the bound 2 for the first parameter. This is often called partial function application.
Example 2: Reordering arguments. std::bind
can also rearrange argument order using placeholders. For instance:
int subtract(int x, int y) {
return x - y;
}
int main() {
// Create a new function that flips the arguments of subtract
auto subtractReversed = std::bind(subtract, _2, _1);
std::cout << subtractReversed(5, 2) << std::endl;
return 0;
}
This will output -3
, because subtractReversed(5,2)
calls subtract(2,5)
(the placeholders _2 and _1 cause the second argument to be used as the first, and vice versa). We effectively bound a new function that subtracts in reverse order.
Why use std::bind
? It can be useful to adapt interfaces. For example, if you have a function that takes 3 parameters but an API needs a callback of 1 parameter, you could bind the other two. Before lambdas existed, std::bind
was a common way to create small function objects on the fly. Note that in modern C++ (C++11 and later), lambda expressions often provide a clearer and more flexible way to achieve the same results. However, understanding std::bind
is important for reading older code and certain functional programming scenarios.
std::mem_fn
std::mem_fn
is a helper that creates a functor from a pointer-to-member. If you have a pointer to a member function or member variable, std::mem_fn
can generate an callable that takes an object (or pointer to object) and calls the member on it. The motivation is to make member function pointers usable in generic algorithms and with std::bind
without manual syntax of .*
or ->*
Consider a member function pointer bool (Widget::*pf)()
pointing to a method of class Widget
. Normally, to call it you would need an object: (obj.*pf)()
. This doesn’t fit well when using it as a callback or in algorithms expecting a callable. std::mem_fn(pf)
yields a function object that you can call like func(obj)
, and it will internally do (obj.*pf)()
(or obj->*pf
if you pass a pointer).
Example: Using std::mem_fn
to call member functions on elements of a container:
#include <functional>
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
struct Person {
string name;
Person(string n): name(n) {}
void greet() const {
cout << "Hello, I'm " << name << endl;
}
};
int main() {
vector people = { Person("Alice"), Person("Bob"), Person("Charlie") };
// Use std::mem_fn to create a callable that calls Person::greet on an object
std::for_each(people.begin(), people.end(), std::mem_fn(&Person::greet));
}
Output:
Hello, I'm Alice
Hello, I'm Bob
Hello, I'm Charlie
Here, std::mem_fn(&Person::greet)
produces a function object that expects a Person
(or reference) and invokes greet()
on it. We pass that to std::for_each
to call greet
for every person in the vector. Without mem_fn
, we might need to write a lambda or use awkward function pointer syntax to achieve the same effect. In general, std::mem_fn
makes it easy to use member function pointers in STL algorithms
Note: Both std::bind
and std::mem_fn
return objects that are Callable (they overload operator()
). The type of the returned object is unspecified (often an implementation-defined template type), so we use auto
to hold them or directly pass them to other functions. Additionally, C++11 introduced std::function
, a general polymorphic function wrapper, which can hold any callable (function pointer, lambda, functor, bind result, etc.), but that is beyond the scope of this lecture.
Quiz: What will the following code print?
int sub(int x,int y){ return x - y; }
auto f = std::bind(sub, std::placeholders::_2, std::placeholders::_1);
std::cout << f(2, 5);
Exercise: Given a regular function int add(int a, int b)
, use std::bind
to create a new callable that adds 5 to its single argument. (In other words, bind the first parameter of add
to 5, creating a unary function.) Show how you would call it to add 5 to the number 10.
Answer:
#include <functional>
#include <iostream>
#include <placeholders>
int add(int a, int b) {
return a + b;
}
int main() {
auto add5 = std::bind(add, 5, std::placeholders::_1);
std::cout << add5(10); // prints 15
return 0;
}
Definition: A lambda expression defines an anonymous function (also known as a closure) in place. You can think of it as a way to write a quick function “on the fly” without naming it. In C++, lambda expressions make code cleaner and more concise by allowing you to define small function-like behaviors inline, at the location where they’re needed. Lambdas are written with the following basic syntax:
[capture list] (parameters) -> return_type {
// function body
}
The capture list (inside []
) is one of the distinctive features of C++ lambdas: it lets the lambda use variables from the surrounding scope. The lambda can capture variables by value (making a copy) or by reference (accessing the original). We’ll discuss this shortly. The parameters and return type work like an ordinary function. (If the return type is omitted, the compiler tries to deduce it from the return
statements.)
Basic example:
auto greet = []() {
std::cout << "Hello from a lambda!" << std::endl;
};
greet(); // invoke the lambda
Here we declared a lambda []() { ... }
that takes no parameters and returns void. We assigned it to auto greet
(the lambda has a unique compiler-generated type), and then called greet()
like a function. This would output “Hello from a lambda!”.
Capturing variables: The power of lambdas is in capturing outside variables. For example:
int factor = 3;
auto multiplyByFactor = [factor](int value) {
return value * factor;
};
std::cout << multiplyByFactor(10) << std::endl; // outputs 30
The lambda captures factor
by value (since we listed factor
in the capture list). Inside the lambda, it has access to a copy of factor
(which is 3 at the moment of lambda creation). Calling multiplyByFactor(10)
returns 10 * 3 = 30
. If we later change the variable factor
outside, it wouldn’t affect the lambda’s captured copy. Alternatively, if we had captured by reference (e.g., [&factor]
), the lambda would refer to the original factor
variable each time it’s called.
We can also use an empty capture list []
(meaning the lambda doesn’t use any outside variables), or capture everything by value [=]
or by reference [&]
as a shorthand. (There are additional nuances: e.g., [=,&var]
to capture “everything by value except var by reference”, etc.) The ability to capture state is what makes lambdas closures.
Use in algorithms – example: Lambdas shine when used with STL algorithms. They allow you to define custom behavior inline without separate functions or functor types. For instance, sorting with a custom comparator:
#include <algorithm>
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector nums = {3, 1, 4, 1, 5, 9};
// Sort in descending order using a lambda comparator
sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b;
});
// Print sorted result
for(int n : nums) cout << n << " ";
cout << endl;
}
We passed a lambda [](int a, int b){ return a > b; }
as the third argument to std::sort
. This lambda tells sort
to order elements in descending order (it returns true if a
should come before b
, i.e., if a
is greater than b
). The result would print the numbers sorted from largest to smallest. Using a lambda here avoids having to write a separate function or functor for this one-off comparator.
Another example – filtering: Suppose we want to remove all negative numbers from a list. We can use std::remove_if
with a lambda:
vector values = {10, -3, 5, -1, 0};
values.erase(
remove_if(values.begin(), values.end(), [](int x){
return x < 0;
}),
values.end()
);
// Now 'values' contains only {10, 5, 0}
The lambda [](int x){ return x < 0; }
serves as the predicate for remove_if
, returning true for elements that should be removed (i.e., negative numbers). After remove_if
, we erase the “removed” tail of the vector. This idiom is concise thanks to lambdas.
Lambda captures and scope: One must be careful that captured variables remain in scope as long as the lambda might be used. If you capture a reference to a local variable and the lambda is stored and used after that variable goes out of scope, that’s undefined behavior. Likewise, capturing by value is safer in such cases but means the lambda works with a snapshot of the value. C++14 introduced init-capture (e.g., [x=move(y)]
) and C++20 expanded capabilities, but those are advanced features.
Lambdas vs. std::bind/functors: Lambdas essentially provide an easier way to create functors. The lambda expression generates an unnamed class with an operator()
under the hood, possibly storing captured variables as members. Compared to std::bind
, lambdas are generally more readable and have fewer surprising behaviors (especially when it comes to argument forwarding and values vs references). In modern code, lambdas often replace most uses of std::bind
and even many uses of custom functors, except in scenarios that require storing and type-erasing callables (where std::function
might be used).
Quiz: What does the following code print?
int a = 5;
auto lam = [a](int b) { return a * b; };
a = 10;
std::cout << lam(3);
Exercise: Use a lambda expression to sort a std::vector<std::string>
by string length (shorter strings come first). Hint: you can call std::sort
with a lambda that compares the lengths of two strings. For example, given {"apple", "kiwi", "banana"}
, the sorted order should be {"kiwi", "apple", "banana"}
.
Answer:
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
using namespace std;
int main() {
vector fruits = {"apple", "kiwi", "banana"};
sort(fruits.begin(), fruits.end(), [](const string &s1, const string &s2) {
return s1.size() < s2.size();
});
// fruits is now {"kiwi", "apple", "banana"}
for (const auto &fruit : fruits)
cout << fruit << " ";
return 0;
}
Concept: Inversion of Control is a design principle in which the normal flow of control is “inverted”: instead of the high-level code calling into lower-level code, the framework or lower-level code calls into your high-level code at designated points In practice, this often means your code provides callbacks that a library or framework will invoke. This pattern is fundamental in event-driven systems, frameworks, and libraries.
With IoC, you often write the pieces (callbacks, handlers, plug-in modules) and a general engine or framework controls when those pieces are called. A common motto for IoC is the “Hollywood principle”: “Don’t call us, we’ll call you.” Instead of your code continuously polling or invoking framework functions, you hand control to the framework (for example, by entering a main loop or calling a sort function) and it will call your function pointers or functor callbacks when needed.
Example – Sorting with comparator (IoC): As mentioned, the standard C qsort
is a simple example of IoC. You pass your comparison function to qsort
, then qsort
takes over the process of sorting – during which it calls your function many times to compare elements The roles are inverted: instead of you driving the sorting algorithm and calling a utility compare function, you let qsort
drive, and it calls your compare function. C++’s std::sort
with a custom comparator works on the same principle.
In C, beginners often first encounter function pointers via qsort
, providing a comparator callback. The qsort
function in the library calls back into user code via that pointer, which is why we call it a “callback”.
IoC is not limited to sorting. Frameworks like GUI toolkits invert control by having a main loop that waits for events and then calls your handlers. Another example: unit testing frameworks – you write test functions following a certain signature, and the framework calls them. In all these cases, your code plugs into a larger system, and the control of when to call your code is handled by that system.
Why IoC is important: It decouples the execution of tasks from the implementation of tasks. The high-level program can rely on generic code (like a sorting algorithm or an event loop) while plugging in custom behavior (like a comparison or event responses). This promotes code reuse and separation of concerns – the framework handles the repetitive or common behavior, and your callback handles the specific needs. It also allows for more asynchronous and flexible program structures, as seen in GUI applications and server architectures.
Coding Challenge – Implementing custom sort with callback: To solidify the idea of IoC and callbacks, consider writing your own sorting function that takes a callback to compare two elements. This is similar to how qsort
works under the hood. We will implement a simple bubble sort that uses a callback to compare elements.
Exercise: Implement a function bubbleSort
that takes an array of int
, its length, and a comparison function pointer with signature bool cmp(int a, int b)
. The cmp
function should return true if a
should come before b
. The bubbleSort
should sort the array in place according to the order defined by cmp
. Test it by sorting in ascending and descending order using appropriate callback functions.
Answer:
#include <iostream>
#include <utility> // for std::swap
using namespace std;
bool ascending(int a, int b) {
return a < b;
}
bool descending(int a, int b) {
return a > b;
}
void bubbleSort(int arr[], int n, bool (*cmp)(int, int)) {
for (int i = 0; i < n - 1; ++i) {
for (int j = 0; j < n - 1 - i; ++j) {
if (!cmp(arr[j], arr[j + 1])) {
swap(arr[j], arr[j + 1]);
}
}
}
}
int main() {
int data[] = {5, 2, 9, 1, 5, 6};
int n = sizeof(data) / sizeof(data[0]);
// Sort in ascending order
bubbleSort(data, n, ascending);
// data is now sorted in ascending order
// Sort in descending order
bubbleSort(data, n, descending);
// data is now sorted in descending order
// Print result of descending sort
for (int i = 0; i < n; ++i) {
cout << data[i] << " ";
}
cout << endl;
return 0;
}
Quiz: Which scenario is an example of Inversion of Control?