C++ / LAMBDAS AND CALLABLE OBJECTS
Function objects and the call operator
Make your own types callable by defining operator(), and know why a functor's distinct type lets the compiler inline calls a function pointer cannot.
What you will learn
- Define operator() on a class so that obj(args) compiles as obj.operator()(args)
- Keep per-call configuration in data members instead of globals or extra parameters
- Mark operator() const so the functor works as a std::set or std::map comparator
- Overload operator() to make one object accept several different argument types
Understanding Function objects and the call operator
A class becomes callable by declaring a member function named operator(). There is nothing magic about the syntax: when square is an object, the expression square(7) is rewritten by the compiler into square.operator()(7), an ordinary member call, so everything you know about member functions still applies. The call operator can take any number of parameters, return any type, be const or non-const, carry default arguments, and be overloaded or templated. Through C++20 the one hard restriction is that it must be a non-static member function, which is why you cannot retrofit callability onto a type you do not own, and never onto int.
The real payoff is in the type system. Each functor class is its own distinct type, so when you hand one to a template such as std::sort or std::count_if, the algorithm is instantiated for that exact type and every f(x) inside it is a direct call to a body the compiler can see and therefore inline. A function pointer carries no such information: the target is a runtime value, so the call is usually indirect, and the pointer has nowhere to store the exponent, limit, or tolerance the operation needs, which forces that state into globals or extra parameters. A functor keeps the state in data members, per object, so square{2} and cube{3} are two independent values of one class.
This is also the mental model for lambdas: the compiler writes an unnamed class with a call operator, turns each capture into a data member, and makes operator() const unless you say mutable. Hand-written functors still earn their keep where a lambda cannot go, namely when you want several overloads of operator() in one object, when the callable must be a named default-constructible type because a container takes it as a template argument like std::set<int, ByLastDigit>, or when the class needs extra members and typedefs, the way std::less<> carries is_transparent.
<iostream>
struct Power {
int exponent; // state: one value per object
long operator()(long base) const { // this is what makes Power callable
long result = 1;
for (int i = 0; i < exponent; ++i) result *= base;
return result;
}
};
template <typename Fn>
void tabulate(Fn f, long from, long to) {
for (long n = from; n <= to; ++n) {
if (n != from) std::cout << ", ";
std::cout << f(n); // compiles to f.operator()(n)
}
std::cout << '\n';
}
int main() {
Power square{2};
const Power cube{3};
std::cout << square(7) << '\n'; // sugar for square.operator()(7)
std::cout << cube.operator()(7) << '\n'; // fine on a const object: operator() is const
tabulate(square, 1, 5);
tabulate(cube, 1, 5);
}operator() is a normal member function with special call syntax, and because every functor is its own type, the compiler resolves and inlines the call while the object carries the state.
Worked examples
One object, several signatures
Overloading operator() gives a single callable that dispatches on its argument type.
<iostream>
<string>
struct Describe {
std::string operator()(int n) const { return "int " + std::to_string(n); }
std::string operator()(double d) const { return "double " + std::to_string(d); }
std::string operator()(const char* s) const { return std::string("text ") + s; }
};
int main() {
Describe d;
std::cout << d(42) << '\n';
std::cout << d(2.5) << '\n';
std::cout << d("hi") << '\n';
std::cout << d('x') << '\n';
}Example explained
Line 1d(42) becomes d.operator()(42), after which normal overload resolution runs over the three operator() members.
Line 2d(2.5) is an exact match for the double overload, so nothing is truncated to int.
Line 3d('x') selects the int overload because char to int is a promotion, which outranks the char to double conversion, printing 120.
Line 4A lambda's closure type declares exactly one operator(), so a callable with three unrelated bodies has to be a hand-written class.
A functor as a container's ordering
Associative containers take the comparator as a template argument, so the functor's type, not a value, defines the order.
<functional>
<iostream>
<set>
struct ByLastDigit {
bool operator()(int a, int b) const { return a % 10 < b % 10; }
};
template <typename Set>
void print(const Set& s) {
const char* sep = "";
for (int x : s) { std::cout << sep << x; sep = " "; }
std::cout << '\n';
}
int main() {
std::set<int, ByLastDigit> byDigit{25, 13, 47, 32};
print(byDigit);
byDigit.insert(45);
std::cout << byDigit.size() << '\n';
std::set<int, std::greater<int>> descending{25, 13, 47, 32};
print(descending);
}Example explained
Line 1std::set<int, ByLastDigit> default-constructs one comparator inside the tree, so every comparison is a direct call to ByLastDigit::operator().
Line 2The keys come out ordered 32, 13, 25, 47 because only the last digit is compared.
Line 3insert(45) is rejected: the comparator reports 25 and 45 as equivalent, and uniqueness in a set is defined by the ordering, never by operator==.
Line 4std::greater<int> is the same technique shipped by the library, and its operator() is const because find and lower_bound are const members.
What a lambda expands into
A capture-by-value lambda behaves like a hand-written struct with a const call operator, but its type is unique.
<iostream>
<type_traits>
struct GreaterThan { // roughly what the compiler writes for the lambda below
int limit;
bool operator()(int v) const { return v > limit; }
};
int main() {
int limit = 10;
auto lam = [limit](int v) { return v > limit; };
GreaterThan hand{limit};
std::cout << lam(12) << hand(12) << lam(3) << hand(3) << '\n';
std::cout << "class type: " << std::is_class_v<decltype(lam)> << '\n';
std::cout << "same type: " << std::is_same_v<decltype(lam), GreaterThan> << '\n';
std::cout << "same size: " << (sizeof(lam) == sizeof(hand)) << '\n';
}Example explained
Line 1The results agree because the capture became a data member and the body became the call operator, exactly as in GreaterThan.
Line 2is_class_v is 1: decltype(lam) names a compiler-generated class type, not a function or pointer type.
Line 3is_same_v is 0: the closure type is unique and unnamed, which is also why two lambdas with identical bodies are different types.
Line 4The sizes match because one captured int becomes one int member, and the lambda's operator() is const since the lambda is not mutable.
Important notes
operator() must be a non-static member function up to C++20; C++23 adds static operator(), which drops the implicit object argument for stateless functors. Either way, only class types can be made callable.
One class can be callable in several ways at once: std::less<> pairs a templated operator() with an is_transparent typedef, which is what lets std::map::find accept a std::string_view for a std::string key without building a temporary.
Common mistakes
Leaving operator() non-const and then using the functor as a std::set or std::map comparator: the container compares through const members such as find and lower_bound, so the build fails with an error about discarding qualifiers on 'this'.
Writing a comparator as a <= b instead of a < b: it is no longer a strict weak ordering, so std::sort can walk past the end of the range (undefined behaviour, often a crash) and std::set silently rejects distinct keys as duplicates.
Passing the type where a value is required, as in std::sort(v.begin(), v.end(), std::greater<int>): that names a type, not an object, and produces a confusing 'expected primary-expression' error instead of pointing at the missing {}.
Try it yourself
Change, predict, then run
Write a struct Clamp with int members lo and hi and a const operator()(int) that pins its argument into [lo, hi], then apply Clamp{0, 10} to the values -5, 3 and 20 with std::transform and print the results, which should be 0 3 10.
Open the C++ workspaceCheck your understanding
Why does std::sort with a functor comparator typically produce faster code than std::sort with a function pointer comparator?
- The functor's exact type is a template argument, so each comparison is a direct call the compiler can see and inline, while a function pointer's target is only known at run time
- A functor lives on the stack whereas a function pointer forces the comparator onto the heap
- operator() defined inside a class is implicitly inline, and a free function is not, so only the functor can ever be inlined
- std::sort has a separate overload specialised for class-type comparators that uses a better algorithm
Show answer
std::sort is a template over the comparator type, so instantiating it with a functor class binds every comparison to one known operator() body, which the optimiser can inline. Option 3 is tempting because in-class definitions really are implicitly inline, but the inline keyword only relaxes the one-definition rule and does not force inlining; the function pointer case loses out because the callee is a runtime value, not because it lacks inline.