C++ Functors
Introduction
In C++, a functor (also known as a function object) is an object that can be called as if it were a function. Functors are instances of classes that overload the function call operator operator()
. This seemingly simple concept provides powerful capabilities that regular functions often lack, such as maintaining state between calls and carrying extra data.
Unlike regular functions, functors can store data and maintain state between calls, making them extremely useful in many contexts, especially when working with the C++ Standard Template Library (STL) algorithms.
Basic Functor Concept
Let's understand the basic structure of a functor:
class MyFunctor {
public:
// The function call operator
return_type operator()(parameters) {
// Function logic here
}
// Potentially other member variables and functions
};
The key to creating a functor is implementing the operator()
(function call operator), which allows objects of the class to be "called" like functions.
Creating and Using Simple Functors
Let's create a simple functor that adds a specific value to a number:
#include <iostream>
// A functor that adds a specific value
class Adder {
private:
int value;
public:
// Constructor to initialize the value to add
Adder(int val) : value(val) {}
// The function call operator
int operator()(int x) const {
return x + value;
}
};
int main() {
// Create functor objects
Adder add5(5);
Adder add10(10);
// Use functors like functions
std::cout << "10 + 5 = " << add5(10) << std::endl;
std::cout << "20 + 10 = " << add10(20) << std::endl;
return 0;
}
Output:
10 + 5 = 15
20 + 10 = 30
In this example, add5
and add10
are functor objects that each store a different value. When called with an argument, they add their stored value to the argument. This demonstrates one of the key advantages of functors: they can store state.
Advantages of Functors
Functors offer several advantages over regular functions:
- State Retention: Functors can maintain state between function calls.
- Parameterization: They can be initialized with different values.
- Type Safety: They provide compile-time type checking.
- Efficiency: Many compilers can inline the function call operator, potentially resulting in faster code.
- Compatibility: They work seamlessly with STL algorithms.
Functors with STL Algorithms
Functors are particularly useful with STL algorithms. Let's see an example using the std::transform
algorithm:
#include <iostream>
#include <vector>
#include <algorithm>
// Functor to multiply by a specific value
class Multiplier {
private:
int factor;
public:
Multiplier(int f) : factor(f) {}
int operator()(int x) const {
return x * factor;
}
};
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> result(numbers.size());
// Use the functor with std::transform
std::transform(numbers.begin(), numbers.end(), result.begin(), Multiplier(3));
// Print the result
std::cout << "Original: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
std::cout << "Multiplied by 3: ";
for (int num : result) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
Output:
Original: 1 2 3 4 5
Multiplied by 3: 3 6 9 12 15
In this example, std::transform
applies the Multiplier(3)
functor to each element of the numbers
vector, storing the results in the result
vector.
Predefined Functors in STL
The STL provides several predefined functors in the <functional>
header:
Arithmetic functors
std::plus<T>
std::minus<T>
std::multiplies<T>
std::divides<T>
std::modulus<T>
std::negate<T>
Comparison functors
std::equal_to<T>
std::not_equal_to<T>
std::greater<T>
std::less<T>
std::greater_equal<T>
std::less_equal<T>
Logical functors
std::logical_and<T>
std::logical_or<T>
std::logical_not<T>
Here's an example using STL functors:
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
int main() {
std::vector<int> numbers = {5, 2, 8, 1, 9};
// Sort in ascending order (default)
std::sort(numbers.begin(), numbers.end());
std::cout << "Ascending: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
// Sort in descending order using greater<> functor
std::sort(numbers.begin(), numbers.end(), std::greater<int>());
std::cout << "Descending: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
Output:
Ascending: 1 2 5 8 9
Descending: 9 8 5 2 1
Real-World Examples
Example 1: Custom Comparison for Sorting
Functors are excellent for defining custom sorting criteria:
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
// A student record
struct Student {
std::string name;
int grade;
Student(const std::string& n, int g) : name(n), grade(g) {}
};
// Functor for comparing students by grade
class CompareByGrade {
public:
bool operator()(const Student& a, const Student& b) const {
return a.grade > b.grade; // Sort in descending order
}
};
int main() {
std::vector<Student> students = {
Student("Alice", 85),
Student("Bob", 92),
Student("Charlie", 78),
Student("Diana", 95),
Student("Eve", 88)
};
// Sort students by grade using the functor
std::sort(students.begin(), students.end(), CompareByGrade());
// Print the sorted list
std::cout << "Students sorted by grades (highest first):" << std::endl;
for (const auto& student : students) {
std::cout << student.name << ": " << student.grade << std::endl;
}
return 0;
}
Output:
Students sorted by grades (highest first):
Diana: 95
Bob: 92
Eve: 88
Alice: 85
Charlie: 78
Example 2: Counting Elements That Match a Condition
Functors can maintain state while processing collections:
#include <iostream>
#include <vector>
#include <algorithm>
// Functor that counts numbers within a specific range
class RangeCounter {
private:
int min, max;
int count;
public:
RangeCounter(int min_val, int max_val)
: min(min_val), max(max_val), count(0) {}
// The function call operator
void operator()(int value) {
if (value >= min && value <= max) {
++count;
}
}
// Getter for the count
int getCount() const {
return count;
}
};
int main() {
std::vector<int> values = {5, 10, 15, 20, 25, 30, 35, 40, 45, 50};
// Count numbers between 15 and 35
RangeCounter counter(15, 35);
// For each element, apply the functor
counter = std::for_each(values.begin(), values.end(), counter);
std::cout << "Numbers between 15 and 35: " << counter.getCount() << std::endl;
return 0;
}
Output:
Numbers between 15 and 35: 5
In this example, the RangeCounter
functor not only processes each element but also maintains a count of elements that fall within a specified range.
Functors vs. Lambda Expressions
While functors are powerful, C++11 introduced lambda expressions, which offer similar functionality with a more concise syntax. Here's a comparison:
#include <iostream>
#include <vector>
#include <algorithm>
// Using a functor
class Multiplier {
private:
int factor;
public:
Multiplier(int f) : factor(f) {}
int operator()(int x) const { return x * factor; }
};
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> result1(numbers.size());
std::vector<int> result2(numbers.size());
// Using a functor
std::transform(numbers.begin(), numbers.end(), result1.begin(), Multiplier(3));
// Using a lambda expression
std::transform(numbers.begin(), numbers.end(), result2.begin(),
[factor=3](int x) { return x * factor; });
// Print results
std::cout << "Using functor: ";
for (int num : result1) std::cout << num << " ";
std::cout << std::endl;
std::cout << "Using lambda: ";
for (int num : result2) std::cout << num << " ";
std::cout << std::endl;
return 0;
}
Output:
Using functor: 3 6 9 12 15
Using lambda: 3 6 9 12 15
While lambda expressions are often more convenient for simple cases, functors still have their place, especially for complex logic that needs to be reused in multiple places.
When to Use Functors
Functors are particularly useful in the following scenarios:
- Stateful operations - When you need to maintain state between function calls
- Complex operations - When the operation logic is complex and would benefit from being encapsulated in a class
- Reusable components - When you need the same operation in multiple places
- Performance-critical code - Functors can sometimes be more optimizable by the compiler than function pointers
- Working with STL algorithms - Many STL algorithms take functors as parameters
Summary
Functors are powerful tools in C++ that combine the flexibility of functions with the stateful nature of objects. Key points to remember:
- Functors are objects that can be called like functions
- They are created by overloading the
operator()
function call operator - Functors can maintain state between calls
- They work seamlessly with STL algorithms
- The STL provides many predefined functors in the
<functional>
header - While lambda expressions offer similar functionality, functors still have their place in C++ programming
Understanding functors is essential for effective C++ programming, especially when working with the STL algorithms.
Exercises
- Create a functor that checks if a number is prime.
- Write a program that uses the
std::count_if
algorithm with a functor to count the number of words in a vector that have more than 5 characters. - Implement a functor that calculates the factorial of a number and use it with
std::transform
. - Create a functor that keeps track of the minimum and maximum values it has seen. Use it with
std::for_each
. - Compare the performance of a functor versus a lambda expression for a compute-intensive task.
Additional Resources
- C++ Reference: Functors
- C++ Reference: STL Algorithms
- Book: "Effective STL" by Scott Meyers
- Book: "C++ Templates: The Complete Guide" by David Vandevoorde and Nicolai M. Josuttis
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)