Lesson 21. Understanding Function Objects

Function objects or functors might sound exotic or intimidating, but they are entities of C++ that you have probably seen, if not also used, without having realized it. In this lesson, you learn

Image The concept of function objects

Image The usage of function objects as predicates

Image How unary and binary predicates are implemented using function objects

The Concept of Function Objects and Predicates

On a conceptual level, function objects are objects that work as functions. On an implementation level, however, function objects are objects of a class that implement operator(). Although functions and function-pointers can also be classified as function objects, it is the capability of an object of a class that implements operator() to carry state (that is, values in member attributes of the class) that makes it useful with Standard Template Library (STL) algorithms.

Function objects as typically used by a C++ programmer working with STL are classifiable into the following types:

Image Unary function—A function called with one argument; for example, f(x). When a unary function returns a bool, it is called a predicate.

Image Binary function—A function called with two arguments; for example, f(x, y). A binary function that returns a bool is called a binary predicate.

Function objects that return a boolean type are naturally suited for use in algorithms that help in decision making. find() and sort() are two such algorithms that you learned about in previous lessons. A function object that combines two function objects is called an adaptive function object.

Typical Applications of Function Objects

It is possible to explain function objects over pages and pages of theoretical explanations. It is also possible to understand how they look and work via tiny sample applications. Let’s take the practical approach and dive straight into the world of C++ programming with function objects or functors!

Unary Functions

Functions that operate on a single parameter are unary functions. A unary function can do something very simple—for example, display an element on the screen. This can be programmed as the following:

// A unary function
template <typename elementType>
void FuncDisplayElement (const elementType& element)
{
    cout << element << ' ';
};

The function FuncDisplayElement accepts one parameter of templatized type elementType that it displays using console output statement std::cout. The same function can also have another representation in which the implementation of the function is actually contained by the operator() of a class or a struct:

// Struct that can behave as a unary function
template <typename elementType>
struct DisplayElement
{
    void operator () (const elementType& element) const
    {
        cout << element << ' ';
    }
};


Tip

Note that DisplayElement is a struct. If it were a class, operator() would need to be given a public access modifier. A struct is akin to a class where members are public by default.


Either of these implementations can be used with the STL algorithm for_each() to print the contents of a collection to the screen, an element at a time, as shown in Listing 21.1.

LISTING 21.1 Displaying the Contents of a Collection on the Screen Using a Unary Function


  0: #include <algorithm>
  1: #include <iostream>
  2: #include <vector>
  3: #include <list>
  4: using namespace std;
  5:
  6: // struct that behaves as a unary function
  7: template <typename elementType>
  8: struct DisplayElement
  9: {
 10:     void operator () (const elementType& element) const
 11:     {
 12:         cout << element << ' ';
 13:     }
 14: };
 15:
 16: int main ()
 17: {
 18:    vector <int> numsInVec{ 0, 1, 2, 3, -1, -9, 0, -999 };
 19:     cout << "Vector of integers contains: " << endl;
 20:
 21:    for_each (numsInVec.begin (),    // Start of range
 22:              numsInVec.end (),        // End of range
 23:              DisplayElement<int> () ); // Unary function object
 24:
 25:     // Display the list of characters
 26:    list <char> charsInList{ 'a', 'z', 'k', 'd' };
 27:    cout << endl << "List of characters contains: " << endl;
 28:
 29:    for_each (charsInList.begin(),
 30:              charsInList.end(),
 31:              DisplayElement<char> () );
 32:
 33:     return 0;
 34: }


Output Image

Vector of integers contains:
0 1 2 3 -1 -9 0 -999
List of characters contains:
a z k d

Analysis Image

Lines 8–14 contain the function object DisplayElement, which implements operator(). The usage of this function object is seen with STL algorithm std::for_each() in Lines 21–23. for_each() accepts three parameters: The first is the starting point of the range, the second is the end of the range, and the third parameter is the function that is called for every element in the specified range. In other words, that code invokes DisplayElement::operator() for every element in the vector numsInVec. Lines 29–31 demonstrate the same functionality with a list of characters.


Note

In Listing 21.1, you may optionally use FuncDisplayElement instead of struct DisplayElement to the same effect:

for_each (charsInList.begin(),
          charsInList.end(),
          FuncDisplayElement<char>);



Tip

C++11 introduced lambda expressions that are unnamed function objects.

A lambda expression version of struct DisplayElement<T> from Listing 21.1 compacts the entire code, including the definition of the struct and its usage, in three lines within main(), replacing Lines 21–24:

// Display elements using lambda expression
for_each (numsInVec.begin(), // Start of range
           numsInVec.end(),  // End of range
   [](int& Element) {cout << element << ' '; });
   // Lambda expression

Thus, lambdas are a fantastic improvement to C++, and you should not miss learning them in Lesson 22, “Lambda Expressions.” Listing 22.1 demonstrates using lambda functions in a for_each() to display the contents of a container, instead of the function object as seen in Listing 21.1.


The real advantage of using a function object implemented in a struct becomes apparent when you are able to use the object of the struct to store information. This is something FuncDisplayElement cannot do the way a struct can because a struct can have member attributes in addition to operator(). A slightly modified version that makes use of member attributes is the following:

template <typename elementType>
struct DisplayElementKeepCount
{
    int count;

    DisplayElementKeepCount ()  // constructor
    {
        count = 0;
    }

    void operator () (const elementType& element)
    {
        ++ count;
        cout << element << ' ';
    }
};

In the preceding snippet, DisplayElementKeepCount is a slight modification over the previous version. operator() is not a const member function anymore as it increments (hence, changes) member count to keep a count of the number of times it was called to display data. This count is made available via the public member attribute count. The advantage of using such function objects that can also store state is shown in Listing 21.2.

LISTING 21.2 Function Object That Holds State


  0: #include<algorithm>
  1: #include<iostream>
  2: #include<vector>
  3: using namespace std;
  4:
  5: template<typename elementType>
  6: struct DisplayElementKeepCount
  7: {
  8:    int count;
  9:
 10:    DisplayElementKeepCount() : count(0) {} // constructor
 11:
 12:    void operator()(const elementType& element)
 13:    {
 14:       ++ count;
 15:       cout << element<< ' ';
 16:    }
 17: };
 18:
 19: int main()
 20: {
 21:     vector<int> numsInVec{ 22, 2017, -1, 999, 43, 901 };
 22:    cout << "Displaying the vector of integers: "<< endl;
 23:
 24:    DisplayElementKeepCount<int> result;
 25:    result = for_each (numsInVec.begin(),
 26:                      numsInVec.end(),
 27:                      DisplayElementKeepCount<int>() );
 28:
 29:    cout << endl << "Functor invoked " << result.count << " times";
 30:
 31:    return 0;
 32: }


Output Image

Displaying the vector of integers:
22 2017 -1 999 43 901
Functor invoked 6 times

Analysis Image

The biggest difference between this sample and the one in Listing 21.1 is the usage of DisplayElementKeepCount() as the return value of for_each(). operator() implemented in struct DisplayElementKeepCount is invoked by algorithm for_each() for every element in the container. It displays the element and increments the internal counter stored in member attribute count. After for_each() is done, you use the object in Line 29 to display the number of times elements were displayed. Note that a regular function used in this scenario instead of the function implemented in a struct would not be able to supply this feature in such a direct way.

Unary Predicate

A unary function that returns a bool is a predicate. Such functions help make decisions for STL algorithms. Listing 21.3 is a sample predicate that determines whether an input element is a multiple of an initial value.

LISTING 21.3 A Unary Predicate That Determines Whether a Number Is a Multiple of Another


  0: // A structure as a unary predicate
  1: template <typename numberType>
  2: struct IsMultiple
  3: {
  4:    numberType Divisor;
  5:
  6:    IsMultiple (const numberType& divisor)
  7:    {
  8:       Divisor = divisor;
  9:    }
 10:
 11:    bool operator () (const numberType& element) const
 12:    {
 13:       // Check if the divisor is a multiple of the divisor
 14:       return ((element % Divisor) == 0);
 15:    }
 16: };


Analysis Image

Here the operator() returns bool and can work as a unary predicate. The structure has a constructor and is initialized to the value of the divisor in Line 8. This value stored in the object is then used to determine whether the elements sent for comparison are divisible by it, as you can see in the implementation of operator(), using the math operation modulus % that returns the remainder of a division operation in Line 14. The predicate compares that remainder to zero to determine whether the number is a multiple.

In Listing 21.4, we make use of the predicate as seen previously in Listing 21.3 to determine whether numbers in a collection are multiples of a divisor input by the user.

LISTING 21.4 Unary Predicate IsMultiple Used with std::find_if() to Find an Element in a vector That Is a Multiple of a User-Supplied Divisor


  0: #include <algorithm>
  1: #include <vector>
  2: #include <iostream>
  3: using namespace std;
  4: // insert code from Listing 21.3 here
  5:
  6: int main ()
  7: {
  8:    vector <int> numsInVec{ 25, 26, 27, 28, 29, 30, 31 };
  9:    cout << "The vector contains: 25, 26, 27, 28, 29, 30, 31" << endl;
 10:
 11:    cout << "Enter divisor (> 0): ";
 12:    int divisor = 2;
 13:    cin >> divisor;
 14:
 15:    // Find the first element that is a multiple of divisor
 16:    auto element = find_if (numsInVec.begin (),
 17:                            numsInVec.end (),
 18:                            IsMultiple<int>(divisor) );
 19:
 20:    if (element != numsInVec.end ())
 21:    {
 22:       cout << "First element in vector divisible by " << divisor;
 23:       cout << ": " << *element << endl;
 24:     }
 25:
 26:    return 0;
 27: }


Output Image

The vector contains: 25, 26, 27, 28, 29, 30, 31
Enter divisor (> 0): 4
First element in vector divisible by  4: 28

Analysis Image

The sample starts with a sample container that is a vector of integers. The usage of the unary predicate is in find_if() as shown in Line 16. In here, the function object IsMultiple() is initialized to a divisor value supplied by the user and stored in variable Divisor. find_if() works by invoking the unary predicate IsMultiple::operator() for every element in the specified range. When the operator() returns true for an element (which happens when that element is divided by 4 and does not produce a remainder), find_if() returns an iterator element to that element. The result of the find_if() operation is compared against the end() of the container to verify that an element was found, as shown in Line 20, and the iterator element is then used to display the value, as shown in Line 23.


Tip

To see how using lambda expressions compact the program shown in Listing 21.4, take a look at Listing 22.3 in Lesson 22.


Unary predicates find application in a lot of STL algorithms such as std::partition() that can partition a range using the predicate, stable_partition() that does the same while keeping relative order of the elements partitioned, find functions such as std::find_if(), and functions that help erase elements such as std::remove_if() that erases elements in a range that satisfy the predicate.

Binary Functions

Functions of type f(x, y) are particularly useful when they return a value based on the input supplied. Such binary functions can be used for a host of arithmetic activity that involves two operands, such as addition, multiplication, subtraction, and so on. A sample binary function that returns the multiple of input arguments can be written as follows:

template <typename elementType>
class Multiply
{
public:
    elementType operator () (const elementType& elem1,
                             const elementType& elem2)
    {
        return (elem1 * elem2);
    }
};

The implementation of interest is again in operator() that accepts two arguments and returns their multiple. Such binary functions are used in algorithms such as std::transform() where you can use it to multiply the contents of two containers. Listing 21.5 demonstrates the usage of such binary functions in std::transform().

LISTING 21.5 Using a Binary Function to Multiply Two Ranges


  0: #include <vector>
  1: #include <iostream>
  2: #include <algorithm>
  3:
  4: template <typename elementType>
  5: class Multiply
  6: {
  7: public:
  8:     elementType operator () (const elementType& elem1,
  9:                              const elementType& elem2)
 10:     {
 11:         return (elem1 * elem2);
 12:     }
 13: };
 14:
 15: int main ()
 16: {
 17:     using namespace std;
 18:
 19:    vector <int> multiplicands{ 0, 1, 2, 3, 4 };
 20:    vector <int> multipliers{ 100, 101, 102, 103, 104 };
 21:
 22:     // A third container that holds the result of multiplication
 23:     vector <int> vecResult;
 24:
 25:     // Make space for the result of the multiplication
 26:     vecResult.resize (multipliers.size());
 27:     transform (multiplicands.begin (), // range of multiplicands
 28:                 multiplicands.end (), // end of range
 29:                 multipliers.begin (),  // multiplier values
 30:                 vecResult.begin (), // holds result
 31:                 Multiply <int> () );    // multiplies
 32:
 33:     cout << "The contents of the first vector are: " << endl;
 34:     for (size_t index = 0; index < multiplicands.size (); ++ index)
 35:         cout << multiplicands [index] << ' ';
 36:     cout << endl;
 37:
 38:     cout << "The contents of the second vector are: " << endl;
 39:     for (size_t index = 0; index < multipliers.size (); ++index)
 40:         cout << multipliers [index] << ' ';
 41:     cout << endl;
 42:
 43:     cout << "The result of the multiplication is: " << endl;
 44:     for (size_t index = 0; index < vecResult.size (); ++ index)
 45:         cout << vecResult [index] << ' ';
 46:
 47:     return 0;
 48: }


Output Image

The contents of the first vector are:
0 1 2 3 4
The contents of the second vector are:
100 101 102 103 104
The result of the multiplication is:
0 101 204 309 416

Analysis Image

Lines 4–13 contain the class Multiply, as shown in the preceding code snippet. In this sample, you use the algorithm std::transform() to multiply the contents of two ranges and store in a third. In this case, the ranges in question are held in std::vector as multiplicands, multipliers, and vecResult. You use std::transform() in Lines 27–31 to multiply every element in multiplicands by its corresponding element in multipliers and store the result of the multiplication in vecResult. The multiplication itself is done by the binary function Multiply::operator() that is invoked for every element in the vectors that make the source and destination ranges. The return value of the operator() is held in vecResult.

This sample thus demonstrates the application of binary functions in performing arithmetic operations on elements in STL containers. The next sample also uses std::transform() but to convert a string to lowercase using function tolower().

Binary Predicate

A function that accepts two arguments and returns a bool is a binary predicate. Such functions find application in STL functions such as std::sort(). Listing 21.6 demonstrates the usage of a binary predicate that compares two strings after reducing them both to lowercase. Such a predicate can be used in performing a case-insensitive sort on a vector of string, for instance.

LISTING 21.6 A Binary Predicate for Case-Insensitive String Sort


  0: #include <algorithm>
  1: #include <string>
  2: using namespace std;
  3:
  4: class CompareStringNoCase
  5: {
  6: public:
  7:    bool operator () (const string& str1, const string& str2) const
  8:    {
  9:       string str1LowerCase;
 10:
 11:       // Assign space
 12:       str1LowerCase.resize (str1.size ());
 13:
 14:       // Convert every character to the lower case
 15:       transform (str1.begin (), str1.end (), str1LowerCase.begin (),
 16:                    ::tolower);
 17:
 18:       string str2LowerCase;
 19:       str2LowerCase.resize (str2.size ());
 20:       transform (str2.begin (), str2.end (), str2LowerCase.begin (),
 21:                   ::tolower);
 22:
 23:       return (str1LowerCase < str2LowerCase);
 24:    }
 25: };


Analysis Image

The binary predicate implemented in operator() first brings the input strings down to lowercase using algorithm std::transform() as shown in Lines 15 and 20 before using the string’s comparison operator, operator <, to return the result of comparison.

You can use this binary-predicate with algorithm std::sort() to sort a dynamic array contained in a vector of string as demonstrated by Listing 21.7.

LISTING 21.7 Using Function Object class CompareStringNoCase to Perform a Case-Insensitive Sort on a vector<string>


  0: // Insert class CompareStringNoCase from Listing 21.6 here
  1: #include <vector>
  2: #include <iostream>
  3:
  4: template <typename T>
  5: void DisplayContents (const T& container)
  6: {
  7:    for (auto element = container.cbegin();
  8:       element != container.cend ();
  9:       ++ element )
 10:       cout << *element << endl;
 11: }
 12:
 13: int main ()
 14: {
 15:    // Define a vector of string to hold names
 16:    vector <string> names;
 17:
 18:    // Insert some sample names in to the vector
 19:    names.push_back ("jim");
 20:    names.push_back ("Jack");
 21:    names.push_back ("Sam");
 22:    names.push_back ("Anna");
 23:
 24:    cout << "The names in vector in order of insertion: " << endl;
 25:    DisplayContents(names);
 26:
 27:    cout << "Names after sorting using default std::less<>: " << endl;
 28:    sort(names.begin(), names.end());
 29:    DisplayContents(names);
 30:
 31:    cout << "Sorting using predicate that ignores case:" << endl;
 32:    sort(names.begin(), names.end(), CompareStringNoCase());
 33:    DisplayContents(names);
 34:
 35:    return 0;
 36: }


Output Image

The names in vector in order of insertion:
jim
Jack
Sam
Anna
Names after sorting using default std::less<>:
Anna
Jack
Sam
jim
Sorting using predicate that ignores case:
Anna
Jack
jim
Sam

Analysis Image

Output displays the contents of the vector in three stages. The first displays contents in order of insertion. The second after a sort() at Line 28 reorders using default sort predicate less<T>, the output demonstrates that jim is not placed after Jack because this is a case-sensitive sort via string::operator<. The last version uses the sort predicate class CompareStringNoCase<> in Line 32 (implemented in Listing 21.6) that ensures that jim comes after Jack notwithstanding the difference in case.

Binary predicates are required in a variety of STL algorithms. For example, std::unique() that erases duplicate neighboring elements, std::sort() that sorts, std::stable_sort() that sorts while maintaining relative order, and std::transform() that can perform an operation on two ranges are some of the STL algorithms that need a binary predicate.

Summary

In this lesson, you gained an insight into the world of functors (or function objects). You learned how function objects are more useful when implemented in a structure or a class than those that are simple functions because the former can also be used to hold state-related information. You got an insight into predicates, which are a special class of function objects, and saw some practical examples that display their utility.

Q&A

Q A predicate is a special category of a function object. What makes it special?

A Predicates always return boolean.

Q What kind of a function object should I use in a call to a function such as remove_if()?

A You should use a unary predicate that would take the value to be processed as the initial state via the constructor.

Q What kind of a function object should I use for a map?

A You should use a binary predicate.

Q Is it possible that a simple function with no return value can be used as a predicate?

A Yes. A function with no return values can still do something useful. For example, it can display input data.

Workshop

The Workshop provides quiz questions to help you solidify your understanding of the material covered and exercises to provide you with experience in using what you’ve learned. Try to answer the quiz and exercise questions before checking the answers in Appendix E, and be certain you understand the answers before going to the next lesson.

Quiz

1. What is the term used for a unary function that returns a bool result?

2. What would be the utility of a function object that neither modifies data nor returns bool? Can you explain using an example?

3. What is the definition of the term function objects?

Exercises

1. Write a unary function that can be used with std::for_each() to display the double of the input parameter.

2. Extend this predicate to indicate the number of times it was used.

3. Write a binary predicate that helps sort in ascending order.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.190.207.144