Introduction

Scott Meyers, an acknowledged C++ expert, has advised against using loops and encouraged the use of the STL algorithms in their place, especially when using the STL containers1. Normally, this makes the purpose of a loop clearer, but sometimes using the algorithms with certain functors and containers can lead to statements that are long and convoluted, with nested functors and templates creating a difficult to read statement.

Using specialized function adaptors can lead to significantly cleaner code while not sacrificing the generic quality of the algorithm. This article presents two specialized function adaptors that work with pair associative containers defined in the STL. These adaptors are an improvement over the fully generic algorithm statement and retain the benefits of algorithms vs. hand written loops.

This article assumes a working knowledge of C++ and some experience with the use of functors, STL algorithms and STL containers. Unless otherwise stated, all types are assumed to be in the std namespace.

STL Algorithm Magic

STL provides what it calls algorithms, essentially specialized loops that allow traversal over generic containers using iterators. Each of these specialized loops is named based on what it does to individual elements of the container it traverses, e.g., for_each simply calls a function, transform outputs the result of a function into another collection, and accumulate adds all the results together. Container types, such as vector or list, provide iterators that allow access to the individual elements, while the algorithm then performs a particular operation to the element.

Each algorithm takes at least one function argument. This function must be defined to take one parameter, the element type contained in the container being iterated over.

The algorithm dereferences the iterator which has the return type of the element in the container. This finds the function overload that takes either a reference or a value of the element type used in the container.

There is no real magic, just a convention decided upon by the library designers that allows a number of different containers and algorithms to interoperate almost seamlessly.

Function Adaptors

Functions passed to algorithms may only take one parameter. What do you do if you need more information passed to a function, or the operation to be performed is to invoke a method on the objects in a collection? You code a wrapper function. Say you want to add 2 to each element in a vector of ints.

Now what if you want to add 3 to each integer in the vector? Another wrapper. Even if you had a variable that an addn function referred to, it would be still only useful for integers. What about adding 0.5 to doubles? Another wrapper, but then another variable, etc. You can probably see where this is headed.

Function adaptors are functors that act as normal function calls from a syntactic standpoint but behave semantically identical to some other language construct, such as a method call (mem_fun) or expression (plus). For example, through the use of the mem_fun function adaptor, it is easy enough to call a member function on each element in a container as seen here:

There are function adaptors to accomplish almost anything, such as calling a function with two parameters (bind2nd), and for function composition f(g()) (compose1).

Pair Associative Containers

Pair associative containers such as map and hash_map are containers that have keys and data. Keys of arbitrary type may be used to reference data within the container. Because they use arbitrary keys to reference data, they require two mandatory template parameters, the key and the data types.

Function arguments to algorithms take only one parameter. To allow for this, the pair associative containers define their element type to be a pair of the key and the data type, which means that a function argument to an algorithm using a pair associative container must look like this:

Standard C++ Solutions

The Standard C++ Library defines certain function adaptors, select1st, select2nd and compose1, that can be used to call a single parameter function with either the key or the data element of a pair associative container.

select1st and select2nd do pretty much what their respective names say they do. They return either the first or second parameter from a pair.

compose1 allows the use of functional composition, such that the return value of one function can be used as the argument to another. compose1(f,g) is the same as f(g(x)).

Using these function adaptors, we can use for_each to call our function.

Considering it was avoiding the for loop for clarity's sake that inspired the use of the STL algorithms in the first place, it doesn't help the case of algorithms vs. hand written loops that the for loop is more clear and concise.

with_data and with_key

with_data and with_key are function adaptors that strive for clarity while allowing the easy use of the STL algorithms with pair associative containers. They have been parameterized much the same way mem_fun has been. This is not exactly rocket science, but it is quickly easy to see that they are much cleaner than the standard function adaptor expansion using compose1 and select2nd.

Using with_data and with_key, any function can be called and will use the data_type or key_type as the function's argument respectively. This allows hash_map, map, and any other pair associative containers in the STL to be used easily with the standard algorithms. It is even possible to use it with other function adaptors, such as mem_fun.

How it works

with_key and with_data have only one difference between them, the element of the pair they extract before calling the function. For this reason, only with_data is explained.

There are two with_data functions, one for function pointers and the other for functors. The compiler overloads which one is called based on the parameter. These each return a specific functor that does the same thing, the difference being the types returned by the operator() function. The one for function pointers (call_func_with_data_t) is able to extract the return type from the template parameters. The functor specific one (call_class_with_data_t) uses the unary_function convention typedefs to determine the argument type (unary_function::argument_type) and return type (unary_function::result_type) of the function passed in.

The operator() method in each functor extracts the second member and calls the function with it. The additional line creating the temporary value is used to make sure that the B type is compatible with the Arg type, since member functions may not be partially specialized.

Using the code

Included in the accompanying zip file is the header file call_with.hpp that defines the two function adaptors. The code is released into the public domain and may be used as however seen fit. It is known to compile on Visual C++ 7.1, gcc 3.2.3, and Comeau 4.3.3.

Conclusion

The with_* function adaptors are not revolutionary, but do help clarify the usage of an algorithm with one of the pair associative containers. Their names state their purpose more clearly than the compose1/select2nd combination. Sometimes, in an effort to be more generic, it pays to be a little more specific.

The author would like to thank Eric Dixon, John Olsen and Nate Robins for their comments and corrections.

References

History

12/21/03 - Initial writing.

1/18/04 - Initial posting.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

Comments and Discussions

Even though I was able to spend only about an hour reviewing the sample code, what a magnificent hour it was!! At the very beginning, I soon realized this was something designed to take the conscientious reader to his/her next higher level, which required a need not just to understanding the code, but to understand the purpose and the aim behind the code.

But understanding the code and its aim is only half the benefit to be gained from any good article. The truly outstanding ones inspire its readers with thoughts and ideas of how they might integrate its use in their own work, and for that I'm not just refering to copying code and making relevant changes at places to fit its intended purpose. I'm talking about demonstrating the full confidence of knowing what you want to accomplish from implementing the core idea of what it is you picked up from having read the sample codes. IOW, it's like the sample code was talking to you when you were reading and studying it, and now it's you continuing the same conversation with confidence, but transformed in the writing of your own code. Doing that, and doing it successfully, will confirm that you have "arrived" at that next higher level.

That was my experience from having read this sample code, and it's an experience I intend to repeat by extracting from it every nuance of knowledge it provides.

To the perspective reader I say, "Don't just glimmer over the code. Let it come to you and speak to you, and even if you cannot speak back to it, listen to what it's saying, for as long as you listen, it will talk!!"