Reusable code saves time and effort in testing and documentation. The C++ algorithms, containers, function objects, and predicates work together to provide the developer with the tools necessary to create such generic programming. In this article, Tracey and Cameron Hughes show some of the specific uses for predicates and function objects in C++.

From the author of

From the author of

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 Very generic function.

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

Regular methods

Function overriding

Polymorphism

2

known

unknown

Function objects

Function predicates

3

unknown

known

Templates

Polymorphism

4

unknown

unknown

Function objects

Function predicates

Templates

Polymorphism

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.