2.2. MetafunctionsIf at this point you have begun to notice some similarity between traits templates and ordinary functions, that's good. The parameters and nested types of a traits template play similar roles at compile time to those played by function parameters and return values at runtime. The binary template from Chapter 1 is certainly function-like. If the "type computation" performed by iterator_traits seems a little too banal to be compared to a function, though, we understand; rest assured that things will quickly get more interesting. Apart from passing and returning types instead of values, traits templates exhibit two significant features that we don't see in ordinary functions:
Still, class templates are enough like functions that we can get some serious mileage out of the analogy. To capture the idea of "class templates-as-functions," we'll use the term metafunctions. Metafunctions are a central abstraction of the Boost Metaprogramming Library, and formalizing them is an important key to its power. We'll be discussing metafunctions in depth in Chapter 3, but we're going to cover one important difference between metafunctions and classic traits right here. The traits templates in the standard library all follow the "multiple return values" model. We refer to this kind of traits template as a "blob," because it's as though a handful of separate and loosely related metafunctions were mashed together into a single unit. We will avoid this idiom at all costs, because it creates major problems. First of all, there's an efficiency issue: The first time we reach inside the iterator_traits for its ::value_type, the template will be instantiated. That means a lot of things to the compiler, but to us the important thing is that at that point the compiler has to work out the meaning of every declaration in the template body that happens to depend on a template parameter. In the case of iterator_traits, that means computing not only the value_type, but the four other associated types as welleven if we're not going to use them. The cost of these extra type computations can really add up as a program grows, slowing down the compilation cycle. Remember that we said type computations would get much more interesting? "More interesting" also means more work for your compiler, and more time for you to drum your fingers on the desk waiting to see your program work. Second, and more importantly, "the blob" interferes with our ability to write metafunctions that take other metafunctions as arguments. To wrap your mind around that, consider a trivial runtime function that accepts two function arguments: template <class X, class UnaryOp1, class UnaryOp2> X apply_fg(X x, UnaryOp1 f, UnaryOp2 g) { return f(g(x)); } That's not the only way we could design apply_fg, though. Suppose we collapsed f and g into a single argument called blob, as follows: template <class X, class Blob> X apply_fg(X x, Blob blob) { return blob.f(blob.g(x)); } The protocol used to call f and g here is analogous to the way you access a "traits blob": to get a result of the "function," you reach in and access one of its members. The problem is that there's no single way to get at the result of invoking one of these blobs. Every function like apply_fg will use its own set of member function names, and in order to pass f or g on to another such function we might need to repackage it in a wrapper with new names. "The blob" is an anti-pattern (an idiom to be avoided), because it decreases a program's overall interoperability, or the ability of its components to work smoothly together. The original choice to write apply_fg so that it accepts function arguments is a good one, because it increases interoperability. When the callable arguments to apply_fg use a single protocol, we can easily exchange them: #include <functional> float log2(float); int a = apply_fg(5.Of, std::negate<float>(), log2); int b = apply_fg(3.14f, log2, std::negate<float>()); The property that allows different argument types to be used interchangeably is called polymorphism; literally, "the ability to take multiple forms."
To achieve polymorphism among metafunctions, we'll need a single way to invoke them. The convention used by the Boost libraries is as follows: metafunction-name<type-arguments...>::type From now on, when we use the term metafunction, we'll be referring to templates that can be "invoked" with this syntax. ![]() |