A Deeper Look at Metafunctions
A Deeper Look at Metafunctions
37
38 CHAPTER 3 A Deeper Look at Metafunctions
Since velocity is just change in distance (l) over time (t), the fundamental dimensions of acceleration
are:
(l/t)/t = l/t 2
And indeed, acceleration is commonly measured in “meters per second squared.” It follows that the
dimensions of force must be:
ml/t 2
and force is commonly measured in kg(m/s2 ), or “kilogram-meters per second squared.” When
multiplying quantities of mass and acceleration, we multiply their dimensions as well and carry
the result along, which helps us to ensure that the result is meaningful. The formal name for this
bookkeeping is dimensional analysis, and our next task will be to implement its rules in the C++ type
system. John Barton and Lee Nackman were the first to show how to do this in their seminal book,
Scientific and Engineering C++ [BN94]. We will recast their approach here in metaprogramming
terms.
that is, mlt −2 . However, if we want to get dimensions into the type system, these arrays won’t do
the trick: they’re all the same type! Instead, we need types that themselves represent sequences of
numbers, so that two masses have the same type and a mass is a different type from a length.
Fortunately, the MPL provides us with a collection of type sequences. For example, we can
build a sequence of the built-in signed integral types this way:
#include <boost/mpl/vector.hpp>
typedef boost::mpl::vector<
signed char, short, int, long> signed types;
How can we use a type sequence to represent numbers? Just as numerical metafunctions pass
and return wrapper types having a nested ::value, so numerical sequences are really sequences of
wrapper types (another example of polymorphism). To make this sort of thing easier, MPL supplies
the int <N> class template, which presents its integral argument as a nested ::value:
#include <boost/mpl/int.hpp>
Namespace Aliases
In fact, the library contains a whole suite of integral constant wrappers such as long and bool ,
each one wrapping a different type of integral constant within a class template.
Now we can build our fundamental dimensions:
typedef mpl::vector<
mpl::int <1>, mpl::int <0>, mpl::int <0>, mpl::int <0>
, mpl::int <0>, mpl::int <0>, mpl::int <0>
> mass;
typedef mpl::vector<
mpl::int <0>, mpl::int <1>, mpl::int <0>, mpl::int <0>
, mpl::int <0>, mpl::int <0>, mpl::int <0>
> length;
...
40 CHAPTER 3 A Deeper Look at Metafunctions
Whew! That’s going to get tiring pretty quickly. Worse, it’s hard to read and verify: The
essential information, the powers of each fundamental dimension, is buried in repetitive syntactic
“noise.” Accordingly, MPL supplies integral sequence wrappers that allow us to write:
Even though they have different types, you can think of these mpl::vector c specializations
as being equivalent to the more verbose versions above that use mpl::vector.
If we want, we can also define a few composite dimensions:
And, incidentally, the dimensions of scalars (like pi) can be described as:
The types listed above are still pure metadata; to typecheck real computations we’ll need to somehow
bind them to our runtime data. A simple numeric value wrapper, parameterized on the number type
T and on its dimensions, fits the bill:
explicit quantity(T x)
: m value(x)
{}
Now we have a way to represent numbers associated with dimensions. For instance, we can say:
quantity<float,length> l( 1.0f );
quantity<float,mass> m( 2.0f );
Note that Dimensions doesn’t appear anywhere in the definition of quantity outside the
template parameter list; its only role is to ensure that l and m have different types. Because they do,
we cannot make the mistake of assigning a length to a mass:
implies that the exponents of the result dimensions should be the sum of corresponding exponents
from the argument dimensions. Division is similar, except that the sum is replaced by a difference.
To combine corresponding elements from two sequences, we’ll use MPL’s transform algorithm.
transform is a metafunction that iterates through two input sequences in parallel, passing an element
from each sequence to an arbitrary binary metafunction, and placing the result in an output sequence.
The signature above should look familiar if you’re acquainted with the STL transform algorithm
that accepts two runtime sequences as inputs:
template <
class InputIterator1, class InputIterator2
, class OutputIterator, class BinaryOperation
>
void transform(
InputIterator1 start1, InputIterator2 finish1
, InputIterator2 start2
, OutputIterator result, BinaryOperation func);
Now we just need to pass a BinaryOperation that adds or subtracts in order to multiply or
divide dimensions with mpl::transform. If you look through the MPL reference manual, you’ll
come across plus and minus metafunctions that do just what you’d expect:
Section 3.1 Dimensional Analysis 43
At this point it might seem as though we have a solution, but we’re not quite there yet. A naive
attempt to apply the transform algorithm in the implementation of operator* yields a compiler
error:
#include <boost/mpl/transform.hpp>
It fails because the protocol says that metafunction arguments must be types, and plus is not a type,
but a class template. Somehow we need to make metafunctions like plus fit the metadata mold.
One natural way to introduce polymorphism between metafunctions and metadata is to employ
the wrapper idiom that gave us polymorphism between types and integral constants. Instead of a
nested integral constant, we can use a class template nested within a metafunction class:
44 CHAPTER 3 A Deeper Look at Metafunctions
struct plus f
{
template <class T1, class T2>
struct apply
{
typedef typename mpl::plus<T1,T2>::type type;
};
};
Definition
A metafunction class is a class with a publicly accessible nested metafunction
called apply.
Whereas a metafunction is a template but not a type, a metafunction class wraps that template
within an ordinary non-templated class, which is a type. Since metafunctions operate on and return
types, a metafunction class can be passed as an argument to, or returned from, another metafunction.
Finally, we have a BinaryOperation type that we can pass to transform without causing a
compilation error:
Now, if we want to compute the force exerted by gravity on a five kilogram laptop computer,
that’s just the acceleration due to gravity (9.8 m/sec2 ) times the mass of the laptop:
quantity<float,mass> m(5.0f);
quantity<float,acceleration> a(9.8f);
std::cout << "force = " << (m * a).value();
Our operator* multiplies the runtime values (resulting in 6.0f), and our metaprogram code uses
transform to sum the meta-sequences of fundamental dimension exponents, so that the result type
contains a representation of a new list of exponents, something like:
Section 3.1 Dimensional Analysis 45
vector c<int,1,1,-2,0,0,0,0>
quantity<float,force> f = m * a;
we’ll run into a little problem. Although the result of m * a does indeed represent a force with
exponents of mass, length, and time 1, 1, and -2 respectively, the type returned by transform isn’t
a specialization of vector c. Instead, transform works generically on the elements of its inputs
and builds a new sequence with the appropriate elements: a type with many of the same sequence
properties as vector c<int,1,1,-2,0,0,0,0>, but with a different C++ type altogether. If you
want to see the type’s full name, you can try to compile the example yourself and look at the error
message, but the exact details aren’t important. The point is that force names a different type, so
the assignment above will fail.
In order to resolve the problem, we can add an implicit conversion from the multiplication’s
result type to quantity<float,force>. Since we can’t predict the exact types of the dimensions
involved in any computation, this conversion will have to be templated, something like:
Unfortunately, such a general conversion undermines our whole purpose, allowing nonsense such
as:
We can correct that problem using another MPL algorithm, equal, which tests that two sequences
have the same elements:
{
BOOST STATIC ASSERT((
mpl::equal<Dimensions,OtherDimensions>::type::value
));
}
Now, if the dimensions of the two quantities fail to match, the assertion will cause a compilation
error.
struct minus f
{
template <class T1, class T2>
struct apply
: mpl::minus<T1,T2> {};
};
Here minus f::apply uses inheritance to expose the nested type of its base class, mpl::
minus, so we don’t have to write:
We don’t have to write typename here (in fact, it would be illegal), because the compiler knows that
dependent names in apply’s initializer list must be base classes.2 This powerful simplification is
known as metafunction forwarding; we’ll apply it often as the book goes on.3
Syntactic tricks notwithstanding, writing trivial classes to wrap existing metafunctions is going
to get boring pretty quickly. Even though the definition of minus f was far less verbose than that
of plus f, it’s still an awful lot to type. Fortunately, MPL gives us a much simpler way to pass
metafunctions around. Instead of building a whole metafunction class, we can invoke transform
this way:
2. In case you’re wondering, the same approach could have been applied to plus f, but since it’s a little subtle, we introduced
the straightforward but verbose formulation first.
3. Users of EDG-based compilers should consult Appendix C for a caveat about metafunction forwarding. You can tell whether
you have an EDG compiler by checking the preprocessor symbol EDG VERSION , which is defined by all EDG-based
compilers.
Section 3.1 Dimensional Analysis 47
Those funny looking arguments ( 1 and 2) are known as placeholders, and they signify
that when the transform’s BinaryOperation is invoked, its first and second arguments will
be passed on to minus in the positions indicated by 1 and 2, respectively. The whole type
mpl::minus< 1, 2> is known as a placeholder expression.
Note
MPL’s placeholders are in the mpl::placeholders namespace and defined in
boost/mpl/placeholders.hpp. In this book we will usually assume that you
have written:
#include<boost/mpl/placeholders.hpp>
using namespace mpl::placeholders;
This code is considerably simpler. We can simplify it even further by factoring the code that
calculates the new dimensions into its own metafunction:
operator/(quantity<T,D1> x, quantity<T,D2> y)
{
return quantity<T, typename divide dimensions<D1,D2>::type>(
x.value() / y.value());
}
quantity<float,mass> m2 = f/a;
float rounding error = std::abs((m2 - m).value());
If we got everything right, rounding error should be very close to zero. These are boring
calculations, but they’re just the sort of thing that could ruin a whole program (or worse) if you got
them wrong. If we had written a/f instead of f/a, there would have been a compilation error,
preventing a mistake from propagating throughout our program.
This might seem like a trivial example, and in fact it is. You won’t find much use for twice in
real code. We hope you’ll bear with us anyway: Because it doesn’t do much more than accept and
invoke a metafunction, twice captures all the essential elements of “higher-orderness” without any
distracting details.
If f is a metafunction class, the definition of twice is straightforward:
Section 3.2 Higher-Order Metafunctions 49
Given the need to sprinkle our code with the template keyword, it would be nice to reduce the
syntactic burden of invoking metafunction classes. As usual, the solution is to factor the pattern into
a metafunction:
To see twice at work, we can apply it to a little metafunction class built around the add pointer
metafunction:
50 CHAPTER 3 A Deeper Look at Metafunctions
But when we look at the implementation of boost::add pointer, it becomes clear that the current
definition of twice can’t work that way.
template <class T>
struct add pointer
{
typedef T* type;
};
We’ll refer to metafunction classes like add pointer f and placeholder expressions like
boost::add pointer< 1> as lambda expressions. The term, meaning “unnamed function ob-
ject,” was introduced in the 1930s by the logician Alonzo Church as part of a fundamental theory
of computation he called the lambda-calculus.4 MPL uses the somewhat obscure word lambda
because of its well-established precedent in functional programming languages.
Although its primary purpose is to turn placeholder expressions into metafunction classes,
mpl::lambda can accept any lambda expression, even if it’s already a metafunction class. In
that case, lambda returns its argument unchanged. MPL algorithms like transform call lambda
internally, before invoking the resulting metafunction class, so that they work equally well with either
kind of lambda expression. We can apply the same strategy to twice:
Now we can use twice with metafunction classes and placeholder expressions:
int* x;
#include <boost/mpl/apply.hpp>
You can think of mpl::apply as being just like the apply1 template that we wrote, with two
additional features:
1. While apply1 operates only on metafunction classes, the first argument to mpl::apply can
be any lambda expression (including those built with placeholders).
2. While apply1 accepts only one additional argument to which the metafunction class will be
applied, mpl::apply can invoke its first argument on any number from zero to five additional
arguments.5 For example:
// binary lambda expression applied to 2 additional arguments
mpl::apply<
mpl::plus< 1, 2>
, mpl::int <6>
, mpl::int <7>
>::type::value // == 13
Guideline
When writing a metafunction that invokes one of its arguments, use mpl::apply
so that it works with lambda expressions.
5. See the Configuration Macros section of the MPL reference manual for a description of how to change the maximum
number of arguments handled by mpl::apply.
Section 3.5 Lambda Details 53
The process of binding argument values to a subset of a function’s parameters is known in the
world of functional programming as partial function application.
When evaluating a lambda expression, MPL checks to see if any of its arguments are themselves
lambda expressions, and evaluates each one that it finds. The results of these inner evaluations are
substituted into the outer expression before it is evaluated.
3.5.1 Placeholders
The definition of “placeholder” may surprise you:
54 CHAPTER 3 A Deeper Look at Metafunctions
Definition
A placeholder is a metafunction class of the form mpl::arg<X>.
3.5.1.1 Implementation
The convenient names 1, 2,. . . 5 are actually typedefs for specializations of mpl::arg that
simply select the Nth argument for any N.6 The implementation of placeholders looks something
like this:
template <>
struct arg<1>
{
template <
class A1, class A2 = void , ... class Am = void >
struct apply
{
typedef A1 type; // return the first argument
};
};
typedef arg<1> 1;
template <>
struct arg<2>
{
template <
class A1, class A2, class A3 = void , ...class Am = void
>
struct apply
{
typedef A2 type; // return the second argument
};
};
6. MPL provides five placeholders by default. See the Configuration Macros section of the MPL reference manual for a
description of how to change the number of placeholders provided.
Section 3.5 Lambda Details 55
typedef arg<2> 2;
}}}
Remember that invoking a metafunction class is the same as invoking its nested apply meta-
function. When a placeholder in a lambda expression is evaluated, it is invoked on the expression’s
actual arguments, returning just one of them. The results are then substituted back into the lambda
expression and the evaluation process continues.
There’s one special placeholder, known as the unnamed placeholder, that we haven’t yet defined:
}}}
The details of its implementation aren’t important; all you really need to know about the unnamed
placeholder is that it gets special treatment. When a lambda expression is being transformed into a
metafunction class by mpl::lambda,
So, for example, every row of Table 3.1 contains two equivalent lambda expressions.
Especially when used in simple lambda expressions, the unnamed placeholder often eliminates
just enough syntactic “noise” to significantly improve readability.
Definition
A placeholder expression is either:
• a placeholder
or
• a template specialization with at least one argument that is a placeholder
expression.
Nullary metafunctions might not seem very important at first, since something like add
pointer<int> could be replaced by int* in any lambda expression where it appears. Not all
nullary metafunctions are that simple, though:
Note that calc ptr seq is a nullary metafunction, since it has transform’s nested ::type.
A C++ template is not instantiated until we actually “look inside it,” though. Just naming calc
ptr seq does not cause it to be evaluated, since we haven’t accessed its ::type yet.
Metafunctions can be invoked lazily, rather than immediately upon supplying all of their argu-
ments. We can use lazy evaluation to improve compilation time when a metafunction result is only
going to be used conditionally. We can sometimes also avoid contorting program structure by naming
an invalid computation without actually performing it. That’s what we’ve done with calc ptr seq
above, since you can’t legally form double&*. Laziness and all of its virtues will be a recurring
theme throughout this book.
3.6 Details
By now you should have a fairly complete view of the fundamental concepts and language of both
template metaprogramming in general and of the Boost Metaprogramming Library. This section
reviews the highlights.
Metafunction forwarding. The technique of using public derivation to supply the nested type of
a metafunction by accessing the one provided by its base class.
58 CHAPTER 3 A Deeper Look at Metafunctions
Metafunction class. The most basic way to formulate a compile-time function so that it can be
treated as polymorphic metadata; that is, as a type. A metafunction class is a class with a
nested metafunction called apply.
MPL. Most of this book’s examples will use the Boost Metaprogramming Library. Like the Boost
type traits headers, MPL headers follow a simple convention:
#include <boost/mpl/component-name.hpp>
If the component’s name ends in an underscore, however, the corresponding MPL header
name does not include the trailing underscore. For example, mpl::bool can be found in
<boost/mpl/bool.hpp>. Where the library deviates from this convention, we’ll be sure to
point it out to you.
Higher-order function. A function that operates on or returns a function. Making metafunctions
polymorphic with other metadata is a key ingredient in higher-order metaprogramming.
Lambda expression. Simply put, a lambda expression is callable metadata. Without some form
of callable metadata, higher-order metafunctions would be impossible. Lambda expressions
have two basic forms: metafunction classes and placeholder expressions.
Placeholder expression. A kind of lambda expression that, through the use of placeholders, enables
in-place partial metafunction application and metafunction composition. As you will see
throughout this book, these features give us the truly amazing ability to build up almost any
kind of complex type computation from more primitive metafunctions, right at its point of use:
// find the position of a type x in some sequence such that:
// x is convertible to 'int'
// && x is not 'char'
// && x is not a floating type
typedef mpl::find if<
some sequence
, mpl::and <
boost::is convertible< 1,int>
, mpl::not <boost::is same< 1,char> >
, mpl::not <boost::is float< 1> >
>
>::type iter;
Placeholder expressions make good on the promise of algorithm reuse without forcing us to
write new metafunction classes. The corresponding capability is often sorely missed in the
runtime world of the STL, since it is often much easier to write a loop by hand than it is to use
standard algorithms, despite their correctness and efficiency advantages.
Section 3.7 Exercises 59
The lambda metafunction. A metafunction that transforms a lambda expression into a corre-
sponding metafunction class. For detailed information on lambda and the lambda evaluation
process, please see the MPL reference manual.
The apply metafunction. A metafunction that invokes its first argument, which must be a lambda
expression, on its remaining arguments. In general, to invoke a lambda expression, you should
always pass it to mpl::apply along with the arguments you want to apply it to in lieu of using
lambda and invoking the result “manually.”
Lazy evaluation. A strategy of delaying evaluation until a result is required, thereby avoiding
any unnecessary computation and any associated unnecessary errors. Metafunctions are only
invoked when we access their nested ::types, so we can supply all of their arguments without
performing any computation and delay evaluation to the last possible moment.
3.7 Exercises
3-0. Use BOOST STATIC ASSERT to add error checking to the binary template presented in
section 1.4.1, so that binary<N>::value causes a compilation error if N contains digits
other than 0 or 1.
3-1. Turn vector c<int,1,2,3> into a type sequence with elements (2,3,4) using transform.
3-2. Turn vector c<int,1,2,3> into a type sequence with elements (1,4,9) using transform.
3-3. Turn T into T**** by using twice twice.
3-4. Turn T into T**** using twice on itself.
3-5. There’s still a problem with the dimensional analysis code in section 3.1. Hint: What happens
when you do:
f = f + m * a;
Show the steps used to arrive at your answers and write tests verifying your assumptions. Did
the library behavior match your reasoning? If not, analyze the failed tests to discover the actual
expression semantics. Explain why your assumptions were different, what behavior you find
more coherent, and why.
3-8*. Our dimensional analysis framework dealt with dimensions, but it entirely ignored the issue
of units. A length can be represented in inches, feet, or meters. A force can be represented in
newtons or in kg m/sec2 . Add the ability to specify units and test your code. Try to make your
interface as syntactically friendly as possible for the user.