Predicates and Function Objects in C++
- What Do We Mean by Generalization?
- What Are Function Objects and Predicates?
- User-Defined Function Objects
- Conclusion
One of the major activities in software development is the process of generalizing components. We like generalized code because it can be reused in various applications. When it has been thoroughly tested and its use properly documented, generalized code saves the user of that code considerable time and effort. Function objects and predicates are tools that aid us in representing components in their most generic form. In this article, we explain how function objects and predicates are used in the process of generalization.
What Do We Mean by ’Generalization’?
The process of generalization can take a specific operation designed for a specific object and transform it into a basic pattern that can apply to families of objects and families of operations. For example, Figure 1 contains a simple four-step process that starts with something very specific and produces something general.
Figure 1 Simple four-step transformation of an equation from specific to general.
Our original statement in Figure 1 multiplies two specific numbers—3 and 3.14. A function that implements this process is only good for those two specific numbers, so we generalize:
- Generalization 1 can take any float and multiply it by 3.14. A function that does this is a little more general than our original statement and is therefore a little more useful.
- Generalization 2 multiplies any two floats. This version is even more useful.
- Generalization 3 really moves things up a notch, by taking any two floats and applying some unspecified operation (op). We can use generalization 3 for more than just multiplication. As long as op produces a value that can be assigned to a float, we can use generalization 3 in a variety of situations.
- Generalization 4 sets us free because it removes the restriction of working with floats. It applies some unspecified operation to some unspecified class of objects and assigns the result to Ans. If generalization 4 is implemented as a template function, it allows the user to supply any two objects, as long as they’re of the same class and some user-defined operation on those objects.
Listing 1 shows a template implementation of generalization 4 from Figure 1.
Listing 1 Very generic function.
template<class T> T op(T X, T Y, SomeFunctionObject<T> A) { T Ans; Ans = A(X,Y); ... return(Ans); }
The process in Figure 1 takes us from a specific object of a specific type to unknown objects of a specific type, and then to unknown objects of unknown types. Likewise, the process in Figure 1 takes us from a specific operation on specific types to a family of operations on unknown types, and finally to an unspecified operation on unknown types.
All of this trickery allows us to write very generic and reusable pieces of code. The key to this kind of abstraction is in expressing the relationships between objects and the operations on those objects as generically as possible. If we’re able to express useful patterns of work for families of objects, then we can achieve highly flexible and reusable code.
Part of the appeal of function objects and predicates is that they allow us to write highly flexible and reusable code. Table 1 provides some hints on where and when function objects and predicates are useful.
Table 1 When to Use Function Objects and Predicates
Case |
Object Type |
Operation |
Approaches |
1 |
known |
known |
|
2 |
known |
unknown |
|
3 |
unknown |
known |
|
4 |
unknown |
unknown |
|
For purposes of this article, we’re most interested in cases 2 and 4 from Table 1. In both cases, the operation to be performed is unspecified and will usually be supplied as a parameter. When a component’s design requires that some operations remain unspecified until runtime, the use of function objects or predicates is usually a good choice.
Case 4 from Table 1 represents a situation in which we need to work with objects of an unspecified type and we need to be able to perform unspecified operations on those objects. This scenario is ideal for the use of function objects and predicates, and is exactly the scenario that the C++ standard container classes and algorithms face. The containers are designed to hold virtually any kind of object, and many of the standard algorithms allow unspecified operations to be performed on the objects that are held in those containers. The algorithms and containers support parameterized programming, also known as generic programming. The goal of parameterized programming is to maximize software reuse by writing software components in as general a form as possible. In C++, when algorithms have parameters for unspecified operations on some kind of object in a container, those parameters are normally met by using function objects and predicates.