C++ Function Objects: Stateful Functors, Comparators, and std::function Overhead

Key takeaways

What functors are, stateful vs function pointers, STL algorithms with predicates, comparison functors, and std::function overhead vs templates.

What is a function object?

Functor is a class that overloads operator(). It can be called like a function and can maintain state, making it more flexible than regular functions.

struct Adder {
    int operator()(int a, int b) const {
        return a + b;
    }
};

int main() {
    Adder add;
    
    cout << add(2, 3) << endl;  // 5
    cout << add(10, 20) << endl;  // 30
}

Why do you need it?:

  • State retention: Save state between function calls
  • Inline Optimization: Faster than function pointers
  • STL Compatible: Used with STL algorithms
  • Type safety: Compile-time type checking
// ❌ Function pointer: cannot hold state
int counter = 0;
void increment() {
    counter++;
}

// ✅ Function object: holds its own state
class Counter {
    int count_ = 0;
public:
    int operator()() {
        return ++count_;
    }
};
Counter counter;
counter();  // 1
counter();  // 2

The function-pointer version needs a global (or otherwise externally shared) counter variable, because a bare function has no storage of its own — every piece of state a plain function touches has to live somewhere outside it. Counter fixes that by attaching the state directly to the callable object itself: count_ is a member, so each Counter instance owns its own independent counter with no global variable required, and you can have as many independent counters as you want just by constructing more Counter objects.

Function object vs function pointer:

Featuresfunction pointerfunction object
Stay status❌ Not possible✅ Available
Inline Optimization❌ Difficulty✅ Available
Type Safe✅ Available✅ Available
Flexibility❌ Low✅ High
PerformanceslowFast
// Function pointer: indirect call
int (*funcPtr)(int, int) = add;
funcPtr(2, 3);  // called indirectly through the pointer

// Function object: can be inlined
Adder adder;
adder(2, 3);  // the compiler can inline this call

The inlining difference is the concrete reason function objects tend to win on performance in hot loops: funcPtr(2, 3) is a call through a pointer whose target the compiler generally can’t determine until runtime (short of whole-program analysis), so it has to emit an actual indirect call instruction. adder(2, 3) calls Adder::operator() on a value of a known, concrete type — the compiler can see exactly which function that is at compile time and is free to inline it, eliminating the call overhead entirely when passed to something like std::sort that’s itself a template.

Functors that carry state

class Counter {
private:
    int count = 0;
    
public:
    int operator()() {
        return ++count;
    }
    
    int getCount() const {
        return count;
    }
};

int main() {
    Counter counter;
    
    cout << counter() << endl;  // 1
    cout << counter() << endl;  // 2
    cout << counter() << endl;  // 3
    
    cout << "Total calls: " << counter.getCount() << endl;
}

Passing functors to STL algorithms

#include <algorithm>

struct IsEven {
    bool operator()(int x) const {
        return x % 2 == 0;
    }
};

int main() {
    vector<int> v = {1, 2, 3, 4, 5, 6};
    
    // count_if
    int evenCount = count_if(v.begin(), v.end(), IsEven());
    cout << "Even count: " << evenCount << endl;
    
    // remove_if
    v.erase(remove_if(v.begin(), v.end(), IsEven()), v.end());
    
    for (int x : v) {
        cout << x << " ";  // 1 3 5
    }
}

IsEven() constructs a temporary functor object right at the call site, which is idiomatic here because IsEven is stateless — there’s nothing to configure, so a fresh temporary is just as good as a named instance and avoids cluttering the surrounding code with a variable that’s used exactly once.

Comparators, filters, accumulators, and converters

A custom comparison for sort

struct Person {
    string name;
    int age;
};

struct CompareByAge {
    bool operator()(const Person& a, const Person& b) const {
        return a.age < b.age;
    }
};

struct CompareByName {
    bool operator()(const Person& a, const Person& b) const {
        return a.name < b.name;
    }
};

int main() {
    vector<Person> people = {
        {"Charlie", 30},
        {"Alice", 25},
        {"Bob", 35}
    };
    
    // Sort by age
    sort(people.begin(), people.end(), CompareByAge());
    
    for (const auto& p : people) {
        cout << p.name << " (" << p.age << ")" << endl;
    }
}

A range filter configured at construction

class RangeFilter {
private:
    int min, max;
    
public:
    RangeFilter(int min, int max) : min(min), max(max) {}
    
    bool operator()(int value) const {
        return value >= min && value <= max;
    }
};

int main() {
    vector<int> v = {1, 5, 10, 15, 20, 25, 30};
    
    // Range 10-20
    RangeFilter filter(10, 20);
    
    auto it = find_if(v.begin(), v.end(), filter);
    if (it != v.end()) {
        cout << "First match: " << *it << endl;  // 10
    }
    
    // Find all matches
    for (int x : v) {
        if (filter(x)) {
            cout << x << " ";
        }
    }
}

This is the case a plain function or function pointer genuinely can’t express as cleanly: RangeFilter’s bounds (min, max) are configured once at construction and then reused across both find_if and the manual loop below it, without needing global variables or a closure. A free function inRange(int value, int min, int max) would need bind or a lambda to fix min/max before it could be handed to find_if, which is exactly the ceremony a stateful functor sidesteps by construction.

An accumulator returned from for_each

class Accumulator {
private:
    int sum = 0;
    
public:
    void operator()(int value) {
        sum += value;
    }
    
    int getSum() const {
        return sum;
    }
};

int main() {
    vector<int> v = {1, 2, 3, 4, 5};
    
    Accumulator acc = for_each(v.begin(), v.end(), Accumulator());
    
    cout << "Sum: " << acc.getSum() << endl;  // 15
}

The detail worth noticing here is that for_each returns the functor it was given, by value, after applying it to every element — that’s what makes Accumulator acc = for_each(...) work at all. This is a deliberate part of for_each’s design specifically to support this exact pattern: pass in a stateful functor, get back the same functor (now carrying accumulated state) as the return value, since the copy passed into for_each itself is otherwise inaccessible once the algorithm returns.

A converter functor for transform

class Multiplier {
private:
    int factor;
    
public:
    Multiplier(int f) : factor(f) {}
    
    int operator()(int value) const {
        return value * factor;
    }
};

int main() {
    vector<int> v = {1, 2, 3, 4, 5};
    vector<int> result(v.size());
    
    transform(v.begin(), v.end(), result.begin(), Multiplier(10));
    
    for (int x : result) {
        cout << x << " ";  // 10 20 30 40 50
    }
}

The standard functors in <functional>

#include <functional>

int main() {
    vector<int> v = {3, 1, 4, 1, 5};
    
    // plus
    int sum = accumulate(v.begin(), v.end(), 0, plus<int>());
    cout << sum << endl;  // 14
    
    // greater (descending order)
    sort(v.begin(), v.end(), greater<int>());
    
    for (int x : v) {
        cout << x << " ";  // 5 4 3 1 1
    }
}

The standard library ships plus, minus, greater, less, and similar functors in <functional> precisely so you don’t have to write struct MyGreater { bool operator()(...) const {...} }; for every common operator every time an algorithm needs one — they’re thin functor wrappers around the built-in operators, provided as ready-made types you can pass directly as template arguments.

Function object vs lambda

// Function object
struct Adder {
    int factor;
    Adder(int f) : factor(f) {}
    int operator()(int x) const { return x + factor; }
};

// Lambda (concise)
auto adder = [factor = 10](int x) { return x + factor; };

int main() {
    Adder add10(10);
    cout << add10(5) << endl;  // 15
    
    cout << adder(5) << endl;  // 15
}

Function Object Advantages:

  • Explicit type
  • Reusable
  • Clear status management

Lambda Advantages:

  • brevity
  • Inline definition
  • Type inference

Under the hood, these two are far more similar than the different syntax suggests — a lambda expression is really syntactic sugar for the compiler generating an unnamed class with an operator(), capturing factor as a member, exactly the way Adder does explicitly. The practical difference is almost entirely about naming and reuse: a lambda’s generated type has no name you can write in code, so it’s awkward to use as an explicit function parameter type or a class member type without auto/templates/std::function, whereas a named functor class can be declared as a type anywhere a type is expected.

const operator(), mutable state, and copy costs

A missing const on operator()

// ❌ Missing const
struct Adder {
    int operator()(int x) {  // not const
        return x + 10;
    }
};
// Can cause problems in STL algorithms that only accept a const object

// ✅ Add const
struct Adder {
    int operator()(int x) const {
        return x + 10;
    }
};

STL algorithms very often take their callable by const& or pass a const copy internally, so a non-const operator() can fail to compile — or worse, compile in some contexts and fail in others depending on how the algorithm happens to be implemented — the moment the algorithm needs to call it through a const reference. Marking operator() const whenever the functor doesn’t need to mutate its own state is the safe default that avoids this entirely.

Mutating state inside a const call

// Removing const would be needed to change state directly...
class Counter {
private:
    mutable int count = 0;  // mutable lets a const operator() still update it
    
public:
    int operator()() const {
        return ++count;
    }
};

mutable is the escape hatch for exactly this tension: the functor needs to satisfy algorithms that expect a const-callable operator() (the requirement from the missing-const case above), but it also genuinely needs to mutate count on every call. Marking count mutable lets operator() keep its const qualifier — which is a promise about the object’s logical state from the caller’s perspective — while still permitting the internal bookkeeping change that logical constness doesn’t really apply to.

Algorithms copy your functor

// ❌ Large embedded state
struct HeavyFunctor {
    vector<int> data;  // large data
    
    int operator()(int x) const {
        // ...
    }
};
// STL algorithms may copy the functor internally

// ✅ Use std::ref to avoid the copy
HeavyFunctor heavy;
for_each(v.begin(), v.end(), ref(heavy));

Most STL algorithms take their functor argument by value, which is fine for something like IsEven (a few bytes, trivially cheap to copy) but becomes a real cost for HeavyFunctor, whose vector<int> data member could be copying megabytes on every single algorithm call that takes it by value. std::ref wraps the functor in a lightweight reference_wrapper, which is cheap to copy — the algorithm copies the wrapper, not the heavy object it refers to, while all the actual calls still operate on the one original heavy instance.

Statistics, conditional transforms, and memoization

Gathering several statistics in one pass

class Statistics {
    int count_ = 0;
    int sum_ = 0;
    int min_ = INT_MAX;
    int max_ = INT_MIN;
    
public:
    void operator()(int value) {
        count_++;
        sum_ += value;
        min_ = std::min(min_, value);
        max_ = std::max(max_, value);
    }
    
    double average() const {
        return count_ > 0 ? static_cast<double>(sum_) / count_ : 0.0;
    }
    
    int min() const { return min_; }
    int max() const { return max_; }
    int count() const { return count_; }
};

// Usage
std::vector<int> data = {10, 20, 30, 40, 50};
Statistics stats = std::for_each(data.begin(), data.end(), Statistics());
std::cout << "Average: " << stats.average() << '\n';  // 30
std::cout << "Min: " << stats.min() << '\n';          // 10
std::cout << "Max: " << stats.max() << '\n';          // 50

This is the accumulator from the for_each example generalized to track several running aggregates at once, and it’s a genuinely useful idiom when you need multiple statistics from a single pass over the data — computing count, sum, min, and max this way touches each element exactly once, versus calling four separate standard algorithms (std::count, std::accumulate, std::min_element, std::max_element) which would traverse the container four separate times.

Conditional transformation with std::function

class ConditionalTransform {
    std::function<bool(int)> predicate_;
    std::function<int(int)> transform_;
    
public:
    ConditionalTransform(
        std::function<bool(int)> pred,
        std::function<int(int)> trans
    ) : predicate_(pred), transform_(trans) {}
    
    int operator()(int value) const {
        if (predicate_(value)) {
            return transform_(value);
        }
        return value;
    }
};

// Usage
std::vector<int> v = {1, 2, 3, 4, 5, 6};
// Double only the even numbers
auto transformer = ConditionalTransform(
    [](int x) { return x % 2 == 0; },
    [](int x) { return x * 2; }
);
std::transform(v.begin(), v.end(), v.begin(), transformer);
// Result: 1, 4, 3, 8, 5, 12

ConditionalTransform stores its predicate and transform as std::function, which is what lets it accept any callable — a lambda, a function pointer, another functor — at the cost of type erasure’s usual overhead (typically a heap allocation for anything beyond a small captureless lambda, plus a virtual-call-like indirection on every invocation). If the predicate and transform are known at compile time and don’t need to vary at runtime, making ConditionalTransform a class template parameterized on the predicate and transform types instead would let the compiler inline through them the same way it does for Adder above — the std::function version trades some of that performance for the flexibility of accepting arbitrary callables chosen at runtime.

A caching wrapper for expensive functions

template<typename Func>
class Memoized {
    Func func_;
    mutable std::map<int, int> cache_;
    
public:
    Memoized(Func func) : func_(func) {}
    
    int operator()(int x) const {
        if (auto it = cache_.find(x); it != cache_.end()) {
            return it->second;
        }
        
        int result = func_(x);
        cache_[x] = result;
        return result;
    }
};

// Usage
int fibonacci(int n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}
Memoized<int(*)(int)> fib(fibonacci);
std::cout << fib(40) << '\n';  // fast (cached)

There’s a subtlety worth flagging in this specific example: fibonacci calls itself recursively (fibonacci(n - 1) + fibonacci(n - 2)), not fib, so only the outermost call at fib(40) benefits from the cache — every recursive call inside fibonacci still recomputes from scratch, uncached, exactly like calling fibonacci(40) directly without any memoization wrapper at all. Genuinely fast, correctly memoized Fibonacci needs the cache to be visible to the recursive calls themselves (typically via a cache passed by reference or a static/member cache the recursive function checks directly), which Memoized as written here doesn’t provide.

FAQ

Q1: When do you use function objects?

A:

  • State retention: Save state between function calls
  • STL algorithm: sort, find_if, transform, etc.
  • Custom Compare/Filter: Complex logic
class RangeFilter {
    int min_, max_;
public:
    RangeFilter(int min, int max) : min_(min), max_(max) {}
    bool operator()(int x) const { return x >= min_ && x <= max_; }
};

Q2: Lambda vs function object?

A:

  • Lambda: simple logic, inline definition
  • Function Object: Complex logic, reuse, explicit typing
// Lambda: simple
auto isEven = [](int x) { return x % 2 == 0; };
// Function object: more involved
class Statistics {
    int count_, sum_;
public:
    void operator()(int x) { count_++; sum_ += x; }
    double average() const { return sum_ / count_; }
};

Q3: What is the performance of function objects?

A: Can be faster than function pointers due to inlining.

// Function pointer: indirect call
std::sort(v.begin(), v.end(), compareFunc);
// Function object: can be inlined
std::sort(v.begin(), v.end(), CompareFunctor());

Q4: Can function objects be reused?

A: Yes. You can pass the same instance to multiple algorithms.

RangeFilter filter(10, 20);
auto it1 = std::find_if(v1.begin(), v1.end(), filter);
auto it2 = std::find_if(v2.begin(), v2.end(), filter);

Q5: Is const required?

A: Recommended when using the STL algorithm. Function objects that do not change state must be declared as const.

// ✅ const recommended
struct IsEven {
    bool operator()(int x) const {
        return x % 2 == 0;
    }
};
// mutable if state needs to change
class Counter {
    mutable int count_ = 0;
public:
    int operator()() const {
        return ++count_;
    }
};

Q6: What are standard function objects?

A: The <functional> header contains std::plus, std::minus, std::greater, etc.

#include <functional>
std::vector<int> v = {3, 1, 4, 1, 5};
// plus
int sum = std::accumulate(v.begin(), v.end(), 0, std::plus<int>());
// greater (descending order)
std::sort(v.begin(), v.end(), std::greater<int>());

Q7: Can function objects be created as templates?

A: Yes.

template<typename T>
struct Less {
    bool operator()(const T& a, const T& b) const {
        return a < b;
    }
};
std::sort(v.begin(), v.end(), Less<int>());

Q8: What are the function object learning resources?

A:

A function object is a class that can be called like a function by overloading operator().