C++ Tutorial - Functors(Function Objects) - 2020
Functors (Function Objects or Functionals) are simply put object + ().
In other words, a functor is any object that can be used with () in the manner of a function.
This includes normal functions, pointers to functions, and class objects for which the () operator (function call operator) is overloaded, i.e., classes for which the function operator()() is defined.
Sometimes we can use a function object when an ordinary function won't work. The STL often uses function objects and provides several function objects that are very helpful.
Function objects are another example of the power of generic programming and the concept of pure abstraction. We could say that anything that behaves like a function is a function. So, if we define an object that behaves as a function, it can be used as a function.
For example, we could define a struct names absValue that encapsulates the operation of converting a value of type float to its absolute valie:
#include <iostream> struct absValue { float operator()(float f) { return f > 0 ? f : -f; } }; int main( ) { using namespace std; float f = -123.45; absValue aObj; float abs_f = aObj(f); cout << "f = " << f << " abs_f = " << abs_f << endl; return 0; }
Output is:
f = -123.45 abs_f = 123.45
As we see from the definition, even though aObj is an object and not a function, we were able to make a call on that object. The effect is to run the overloaded call operator defined by the object absValue. The operator takes a float value and returns its absolute value. Note that the function-call operator must be declared as a member function.
So, Objects of class types, like the absValue object, that define the call operator are referred to as function object.
Let's look at another example simulating a line. The class object is working as a functor taking x for a given y-intercept(b) and slope(a) giving us the corresponding y coordinate.
y = ax + b
Here is the code:
#include <iostream> using namespace std; class Line { double a; // slope double b; // y-intercept public: Line(double slope = 1, double yintercept = 1): a(slope),b(yintercept){} double operator()(double x){ return a*x + b; } }; int main () { Line fa; // y = 1*x + 1 Line fb(5.0,10.0); // y = 5*x + 10 double y1 = fa(20.0); // y1 = 20 + 1 double y2 = fb(3.0); // y2 = 5*3 + 10 cout << "y1 = " << y1 << " y2 = " << y2 << endl; return 0; }
Here y1 is calculated using the expression 1 * 20 + 1 and y2 is calculated using the expression 5 * 3 + 10. In the expression a *x + b, the values for b and a come from the constructor for the object, and the value of x comes from the argument to operator()().
Now that we have a little bit of taste for functor, let's step back and think. So, what's the behavior of a function? A functional behavior is something that we can call by using parentheses and passing arguments. For example:
func(arg1, arg2);
If we want objects to behave this way, we have to make it possible to call them by using parentheses and passing arguments. It's not that difficult. All we have to do is to define operator () with the appropriate parameter types:
Class X { public: // define "function call" operator return-value operator() (arguments) const; ... };
Then we can use object of this class to behave as a function that we can call:
X fn; ... fn(arg1, arg2); // call operator () for function object fn
This call is equivalent to:
fn.operator()(arg1,arg2); // call operator () for function object fn
Here is a function object example.
#include <iostream> #include <vector> #include <algorithm> using namespace std; class Print { public: void operator()(int elem) const { cout << elem << " "; } }; int main () { vector<int> vect; for (int i=1; i<10; ++i) { vect.push_back(i); } Print print_it; for_each (vect.begin(), vect.end(), print_it); cout << endl; return 0; }
The for_each function applied a specific function to each member of a range:
for_each (vect.begin(), vect.end(), print_it);
In general, the 3rd argument could be a functor, not just a regular function. Actually, this raises a question. How do we declare the third argument? We can't declare it as a function pointer because a function pointer specifies the argument type. Because a container can contain just about any type, we don't know in advance what particular type should be used. The STL solves that problem by using template.
The class Print defines object for which we can call operator () with an int argument. The expression
print_itin the statement
for_each (vect.begin(), vect.end(), print_it);creates a temporary object of this class, which is passed to the for_each() algorithm as an argument. The for_each algorithm looks like this:
template<class Iterator, class Function> Function for_each(Iterator first, Iterator last, Function f) { while (first != last) { f(*first); ++first; } return f; }for_each uses the temporary function f to call f(*first) for each element first. If the third parameter is an ordinary function, it simply calls it with *first as an argument. If the third parameter is a function object, it calls operator() for the function object f with *first as an argument. Thus, in this example for_each() calls:
print_it::operator()(*first)The output is:
1 2 3 4 5 6 7 8 9
Here are some advantages of function object listed in "The C++ Standard Library" by Nicolai M. Josuttis.
- Function object are "smart functions."
Objects that behave like pointers are smart pointers. This is similarly true for objects that behave like functions: They can be "smart functions" because they may have abilities beyond operator (). Function objects may have other member functions and attributes. This means that function objects have a state. .... - Each function object has its own type.
Ordinary functions have different types only when their signatures differ. However, function objects can have different types when their signatures are the same. In fact, each functional behavior defined by a function object has its own type. This is a significant improvement for generic programming using templates because you can pass functional behavior as a template parameter. ... - Function objects are usually faster than ordinary functions.
The concept of templates usually allows better optimization because more details are defined at compile time. Thus, passing function objects instead of ordinary functions often results in better performance.
A template nontype parameter is a constant value inside the template definition. We can use a nontype parameter when we need constant expressions. In the example, we need that to specify the size of an array. So, when arrayInit is called, the compiler figures out the value of the nontype parameter from the array argument.
Here is an example using integer as a non-type parameters. In the code below, we want to assign values for all the elements of a vector. So, we set the template parameter with an integer value as non-type, and as we traverse each element of the vector container, we set the value we want by calling setValue().
#include <iostream> #include <vector> #include <algorithm> using namespace std; template <int val> void setValue(int& elem) { elem = val; } template <typename T> class PrintElements { public: void operator()(T& elm) const {cout << elm << ' ';} }; int main() { int size = 5; vector<int> v(size); PrintElements<int> print_it; for_each(v.begin(), v.end(), print_it); for_each(v.begin(), v.end(), setValue<10>); for_each(v.begin(), v.end(), print_it); return 0; }
Output:
0 0 0 0 0 10 10 10 10 10
Here is a similar example but this time it's adding some value to each element of the vector at runtime:
#include <iostream> #include <vector> #include <algorithm> using namespace std; template <typename T> class Add { T x; public: Add(T xx):x(xx){} void operator()(T& e) const { e += x; } }; template <typename T> class PrintElements { public: void operator()(T& elm) const { cout << elm << ' ';} }; int main() { int size = 5; vector<int> v; for(int i = 0; i < size; i++) v.push_back(i); PrintElements<int> print_it; for_each(v.begin(), v.end(), print_it); cout << endl; for_each(v.begin(), v.end(), Add<int>(10)); for_each(v.begin(), v.end(), print_it); cout << endl; for_each(v.begin(), v.end(), Add<int>(*v.begin())); for_each(v.begin(), v.end(), print_it); return 0; }
Output:
0 1 2 3 4 10 11 12 13 14 20 21 22 23 24
The first call to Add(10) creates a Add type temporary object which is initialized with x = 10. Then, it adds its member value 10 to each of the element of the vector v while traversing. The second call to Add(*v.begin()) sets the member of the temporary object to v.begin() element, and then adds that value to each of the element of the vector.
STL refines functor concepts as follows:
- A generator is a functor that can be called with no argument.
- A unary function is a functor that can be called with one argument.
- A binary function is a functor that can be called with two arguments.
The print_it functor for for_each() we used in the previous section is a unary function because it is applied to one container element at a time.
A special auxiliary function for algorithm is a predicate. Predicates are functions that return a Boolean value (or something that can be implicitly converted to bool). In other words, a predicate class is a functor class whose operator() function is a predicate, i.e., its operator() returns true or false.
Predicates are widely used in the STL. The comparison functions for the standard associative containers are predicates, and predicate functions are commonly passed as parameters to algorithms like find_if. Depending on their purpose, predicates are unary or binary.
- A unary function that returns a bool value is a predicate.
- A binary function that returns a bool value is a binary predicate.
A typical example of predefined function object in STL is set. It sorts element in increasing order because by default it's using less(<) sorting criteria. So, when we do:
set<int> mySet;
internal code looks like this:
set<int, less<int> > mySet;
So, we want to store our elements in decreasing order in a set, we do:
set<int, greater<int> > mySet;
In the example below, the negate<int>() creates a function object from the predefined template class negate. It simply returns the negated int element.
The second transform() algorithm combines the elements from two vector containers by using multiplies<int>() operation. Then it writes the results into the 3rd vector container.
#include <iostream> #include <vector> #include <algorithm> using namespace std; template <typename T> class PrintElements { public: void operator()(T& elm) const { cout << elm << ' ';} }; int main() { PrintElements<int> print_it; int size = 5; vector<int> v; for(int i = 0; i < size; i++) v.push_back(i); for_each(v.begin(), v.end(), print_it); cout << endl; transform(v.begin(), v.end(), // source v.begin(), // destination negate<int>()); // operation for_each(v.begin(), v.end(), print_it); cout << endl; transform(v.begin(), v.end(), // source v.begin(), // second source v.begin(), // destination multiplies<int>()); // operation for_each(v.begin(), v.end(), print_it); cout << endl; return 0; }
Output:
0 1 2 3 4 0 -1 -2 -3 -4 0 1 4 9 16
Here are the two types of transform() algorithms:
- Transforming elements
OutputIterator transform ( InputIterator source.begin(), InputIterator source.end(), OutputIterator destination.begin(), UnaryFunc op )
- Combining elements of two sequences
OutputIterator transform ( InputIterator1 source1.begin(), InputIterator1 source1.end(), InputIterator2 source2.begin() OutputIterator destination.begin(), BinaryFunc op )
Function adapter converts some other interface to an interface used by STL.
Here is the declaration of bind2nd():
template <class Operation, class T> binder2nd<Operation> bind2nd (const Operation& op, const T& x);
It returns function object with second parameter bound. This function constructs an unary function object from the binary function object op by binding its second parameter to the fixed value x.
The function object returned by bind2nd() has its operator() defined such that it takes only one argument. This argument is used to call binary function object op with x as the fixed value for the second argument.
#include <iostream> #include <vector> #include <algorithm> #include <iterator> using namespace std; struct PrintElm { void operator()(int & elm) const { cout << elm << ' ';} }; int main() { int size = 10; vector<int> v; for(int i = 0; i < size; i++) v.push_back(i); for_each(v.begin(), v.end(), PrintElm()); cout << endl; replace_if(v.begin(), v.end(), bind2nd(equal_to<int>(),0), 101); for_each(v.begin(), v.end(), PrintElm()); cout << endl; v.erase( remove_if(v.begin(), v.end(), bind2nd(less<int>(), 3)), v.end() ); for_each(v.begin(), v.end(), PrintElm()); cout << endl; transform(v.begin(), v.end(), ostream_iterator<int>(cout, " "), negate<int>()); return 0; }
Output:
0 1 2 3 4 5 6 7 8 9 101 1 2 3 4 5 6 7 8 9 101 3 4 5 6 7 8 9 -101 -3 -4 -5 -6 -7 -8 -9
Here are additional examples of bind2nd():
- To count all the elements within a vector that are less than or equal to 100, we can use count_if():
count_if(vec.begin(), vec.end(), bind2nd(less_equal<int>(), 100));
The 3rd argument uses bind2nd() function adaptor. This adaptor returns a function object that applies the <= operator using 100 as the right hand operand. So, the call to count_if() counts the number of elements in the input range (from vec.begin() to vec.end()) that are less than or equal to 100. - We can negate the binding of less_equal:
count_if(vec.begin(), vec.end(), not1(bind2nd(less_equal<int>(), 100)));
As in the previous sample, we first bind the second operand of the less_equal object to 100, transforming that binary operation into a unary operation. Then, we negate the return from the operation using not1. So, what it does is that each element will be tested to see if it is <= 100. Then, the truth value of the result will be negated. Actually, the call counts those elements that are not <= 100.
Here is another sample of using bind2nd() with transform(). The code multiplies 10 to each element of an array and outputs them in Print() function:
#include <iostream> #include <iterator> #include <algorithm> using namespace std; template <typename ForwardIter> void Print(ForwardIter first, ForwardIter last, const char* status) { cout << status << endl; while(first != last) cout << *first++ << " "; cout << endl; } int main() { int arr[] = {1, 2, 3, 4, 5}; Print(arr, arr+5, "Initial values"); transform(arr, arr+5, arr, bind2nd(multiplies<int>(), 10)); Print(arr, arr+5, "New values:"); return 0; }
Output:
$ g++ -o bind bind.cpp $ ./bind Initial values 1 2 3 4 5 New values: 10 20 30 40 50
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization