Noeud:Function Objects - in a Nutshell, Noeud « Next »:, Noeud « Up »:Generic Algorithms and Function Objects



Function Objects - in a Nutshell

If you are familiar with the concept of a function pointer, then function objects aren't too disimilar. The basic concept holds for both: we can create many different instances of the same function, but each operating with a different state. However, the difference is that a function object is not a function - it is an object that behaves like a function.

We'll begin by describing some predefined function objects and look at how they work, and afterwards look at how to create our own function objects. This will all tie in with the next section, Introducing Generic Algorithms, which will show you how to exploit some of the function objects we'll look at in this chapter - many generic algorithms accept a function object as one of their arguments.

The STL provides a number predefined function objects. Our purpose here is to describe a few of them; the rest will be provided in the Some Predefined Function Objects.

Right. So we said that a function object is a class that behaves like a function. But it isn't actually a function; we're just wrapping up functional behaviour in a class by overloading the function call operator() so that we can call the class like we'd call a function. Let's look at a commonly used function, negate.

negate returns the negative value of what was passed in. Pretty simple really; let's look at the declaration in stl_function.h (don't despair at it - we'll explain it in due course!)1:

     template <class T>
     struct negate : public unary_function<T, T>
     {
       T operator()(const T& x) const { return -x; }
     };
     

Note that negate inherits from unary_function - in other words, it takes one parameter. Likewise, a binary function (which we'll meet soon) takes two parameters. negate deals with one type, T, the type we'll be passing in. This could be anything; an int, string, an Address object, and so on, with the following restriction: we must be able to negate the value of what we pass in (so for example we'd expect to get -5 if we passed in 5 as an int). We use the overloaded function operator operator() to be able to call negate as a function. Obviously, when we pass in x, we return -x.

Let's see negate in use with a vector of integers, defining our own method named with_each to be able to walk through the elements of the container:

     /* function1.cc
      * Compiled using g++ function1.cc -o function1 */
     #include <vector>
     #include <functional>
     
     /* Define our own method that uses a unary function 'fn' on
      * a range of elements: */
     template <class InputIterator, class Function>
     void with_each(InputIterator beg, InputIterator end, Function fn)
     {
       for( ; beg != end; ++beg)
         cout << fn(*beg) << endl;
     }
     int main()
     {
       std::vector<int> v;
       v.push_back(10);
       v.push_back(2);
       v.push_back(4);
       with_each(v.begin(), v.end(), negate<int>());
       return 0;
     }
     
     Example 3.15: function1.cc
     

Here's the output:

     $ ./function1
     -10
     -2
     -4
     

The key to understanding what is going on involves looking at the with_each algorithm. Notice to start with that the third parameter of with_each in main takes negate<int>() as it's argument. This creates an instance of the negate function. The body of with_each works as follows:

       for( ; beg != end; ++beg)
         cout << fn(*beg) << endl;
     

Here, fn is obviously negate, operating on the range of elements beg to (but not including2), end. It takes *beg as it's argument, which is a pointer to an integer. So all we're doing is saying "apply fn with argument *beg" - or, more precisely - "apply negate with argument *beg". More generally, with_each takes a function object, fn, so we could pass any unary function object into with_each.

negate is a unary function object. What about binary function objects? Well, there's not that much difference except that they take two arguments instead of one, surprise surprise. Let's look at a function object that takes two arguments, called greater. greater takes elements x and y and returns true if x is greater than y, false otherwise. It's definition is simple, and isn't too disimilar to negate:

     template <class T>
     struct greater : public binary_function<T, T, bool>
     {
       bool operator()(const T& x, const T& y) const
                     { return x > y;}
     };
     

We're going to hook greater into an example similar from earlier, using greater as an argument to a modified with_each method:

     /* function2.cc
      * Compiled using g++ function2.cc -o function2 */
     #include <vector>
     #include <functional>
     
     /* Define our own method that uses a binary function 'fn' on
      * a range of elements: */
     template <class InputIterator, class T, class Function, class Message>
     void with_each(InputIterator beg, InputIterator end,
     	       T val, Function fn, Message msg)
     {
       for( ; beg != end; ++beg)
         if (fn(*beg, val))
           cout << *beg << msg << val << endl;
     }
     
     int main()
     {
       std::vector<int> v;
       v.push_back(10);
       v.push_back(2);
       v.push_back(14);
       with_each(v.begin(), v.end(), 7, greater<int>(), " is greater than ");
       return 0;
     }
     
     Example 3.16: function2.cc
     

Again, the output is fairly obvious:

     $ ./function2
     10 is greater than 7
     14 is greater than 7
     
... and isn't really anything to get excited about. We could have even passed in the pre-defined less function object, with Message being " is less than " and the results would again have been obvious. But the point is that function objects provide great flexibility and power, more so when we introduce generic algorithms.

This has been a whistle-stop tour to look describe some function objects and look at the basic principles involved. The idea is simple - create a class that does what you want it to do, placing the functionality in the operator() body. It doesn't need to be a template class - although it's always advantageous if you can do so. If you're perplexed about why we used the with_each method with the function objects, don't worry because we're actually mimicing a generic algoritm called for_each, which we'll encounter soon - you'll see (in Introducing Generic Algorithms) how we'll use function objects with generic algorithms in a similar manner to how we just used with_each.

This may not have seemed much of an in-depth or informative explanation of function objects, but we're really holding back until we look at generic algorithms. All you need to remember is that function objects wrap up functional behaviour in a class, and we can make calls to that class by calling the overloaded operator.


Notes de bas de page

  1. The declaration of negate has been modified to make it a little more readable; the exact text of negate will vary from implementation to implementation.

  2. Recall from A Crash Course in Iterators that end points past the last element in a container.