Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

Front Page / Tutorial: Metafunctions and Higher-Order Metaprogramming / Dimensional Analysis / Implementing Multiplication

Implementing Multiplication

Multiplication is a bit more complicated than addition and subtraction. So far, the dimensions of the arguments and results have all been identical, but when multiplying, the result will usually have different dimensions from either of the arguments. For multiplication, the relation:

(xa)(xb) == x (a + b)

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.

template <class Sequence1, class Sequence2, class BinaryOperation>
struct transform;  // returns a 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 the MPL reference manual, you'll come across plus and minus metafunctions that do just what you'd expect:

#include <boost/static_assert.hpp>
#include <boost/mpl/plus.hpp>
#include <boost/mpl/int.hpp>
namespace mpl = boost::mpl;

BOOST_STATIC_ASSERT(( 
    mpl::plus<
        mpl::int_<2>
      , mpl::int_<3>
    >::type::value == 5
));

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>

template <class T, class D1, class D2>
quantity< 
    T
  , typename mpl::transform<D1,D2,mpl::plus>::type
>
operator*(quantity<T,D1> x, quantity<T,D2> y) { ... }

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:

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:

template <class T, class D1, class D2>
quantity< 
    T
  , typename mpl::transform<D1,D2,plus_f>::type  // new dimensions
>
operator*(quantity<T,D1> x, quantity<T,D2> y)
{
    typedef typename mpl::transform<D1,D2,plus_f>::type dim;
    return quantity<T,dim>( x.value() * y.value() );
}

Now, if we want to compute the force exterted by gravity on a 5 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:

mpl::vector_c<int,1,1,-2,0,0,0,0>

However, if we try to write:

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 mpl::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:

template <class T, class Dimensions>
struct quantity
{
    // converting constructor
    template <class OtherDimensions>
    quantity(quantity<T,OtherDimensions> const& rhs)
      : m_value(rhs.value())
    {
    }
    ...

Unfortunately, such a general conversion undermines our whole purpose, allowing nonsense such as:

// Should yield a force, not a mass!
quantity<float,mass> bogus = m * a;

We can correct that problem using another MPL algorithm, equal, which tests that two sequences have the same elements:

template <class OtherDimensions>
quantity(quantity<T,OtherDimensions> const& rhs)
  : m_value(rhs.value())
{
    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.