6.1. Algorithms, Idioms, Reuse, and AbstractionAbstraction can be defined as generalization away from specific instances or implementations, and toward the "essence" of an object or process. Some abstractions, like that of an STL iterator, become so familiar that they can be called idiomatic. In software design, the idea reuse achieved through idiomatic abstractions can be just as important as code reuse. The best libraries provide both reusable code components and reusable idioms. Because most of them operate at the relatively low level of sequence traversal, it's easy to miss the fact that the STL algorithms represent powerful abstractions. In fact, it's commonly arguednot entirely without causethat for trivial tasks, the algorithms are inferior to handwritten loops. For example:[1]
// "abstraction" std::transform( v.begin(), v.end(), v.begin() , std::bind2nd(std::plus<int>(),42) ); // handwritten loop typedef std::vector<int>::iterator v_iter; for (v_iter i = v.begin(), last = v.end(); i != last; ++i) *i += 42; So, what's wrong with the use of transform above?
These weaknesses, however, can be overcome quite easily. For example, we can use the Boost Lambda library, which inspired MPL's compile time lambda expressions, to simplify and clarify the runtime function object:[2]
std::transform(v.begin(), v.end(), v.begin(), _1 + 42); or even: std::for_each(v.begin(), v.end(), _1 += 42); Both statements do exactly the same thing as the raw loop we saw earlier, yet once you are familiar with the idioms of the Lambda library, iterators, and for_each, the use of algorithms is far clearer. We could raise the abstraction level a bit further by rewriting STL algorithms to operate on whole sequences (like the MPL algorithms do), but let's stop here for now. From the simplification above, you can already see that many of the problems with our example weren't the fault of the algorithm at all. The real culprit was the STL function object framework used to generate the algorithm's function argument. Setting aside those problems, we can see that these "trivial" algorithms abstract away several deceptively simple low-level details:
These all seem easy enough to get right when you consider a single loop, but when that pattern is repeated throughout a large project the chance of errors grows rapidly. The optimizations mentioned above only tend to increase that risk, as they generally introduce even more low-level detail. More importantly, the use of for_each achieves separation of concerns: the common pattern of traversing and modifying all the elements of a sequence is neatly captured in the name of the algorithm, leaving us only to specify the details of the modification. In the compile time world, this division of labor can be especially important, because as you can see from the binary template we covered in Chapter 1, coding even the simplest repeated processes is not so simple. It's a great advantage to be able to use the library's pre-written algorithms, adding only the details that pertain to the problem you're actually trying to solve. When you consider the complexity hidden behind algorithms such as std::lower_bound, which implements a customized binary search, or std::stable_sort, which gracefully degrades performance under low memory conditions, it's much easier to see the value of reusing the STL algorithms. Even if we haven't convinced you to call std::for_each whenever you have to operate on all elements of a sequence, we hope you'll agree that even simple sequence algorithms provide a useful level of abstraction. |