Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reasoning about boost mpl placeholders

Tags:

c++

boost-mpl

The Tutorial: Metafunctions and Higher-Order Metaprogramming section of the Boost MPL library documentation states that transform can be invoked like so

typename mpl::transform<D1,D2, mpl::minus<_1,_2> >::type

where the placeholders _1 and _2 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.

I've been reading this over and over again for almost a month and I still don't understand it.

What values exactly do the placeholders _1 and _2 have? D1 and D2? If so, why not write mpl::minus<D1,D2>? Also considering that the placeholders are defined as typedef arg<1> _1; and typedef arg<2> _2; and consequently the original expression in my mind amounts to

typename mpl::transform<D1,D2, mpl::minus<<arg<1>,<arg<2> > >::type

I'm sure I'm thinking about placeholders the wrong way. I'd appreciate some guidance here.

like image 617
Olumide Avatar asked Mar 18 '23 11:03

Olumide


1 Answers

Indeed, you're thinking about placeholders the wrong way.

mpl::minus is a template in MPL's metalanguage that symbolically represents (or corresponds to) a certain high-level behavior, namely, subtraction. You're thinking of it as if it's a non-meta construct such as a function

int minus(int a, int b) { return a - b; }

but it's not. (The standard C++11 library does have something kind of like that, called std::minus<>, but that's not what mpl::minus does!)

mpl::minus represents the subtraction operation at a higher level of abstraction. Don't worry for another couple of paragraphs about how mpl::minus is implemented. Just think about what it represents, which is subtraction of two things.

Ah, but which two things? Well, mpl::minus lets you specify those things as template parameters. For example,

mpl::minus<mpl::int_<7>, mpl::int_<3>>

expands to a type whose member typedef type is the same as mpl::int_<4>.

Okay, but in the dimensional analysis example from Boost, they don't have just two things; they have sequences D1 and D2 of dimensions. (This is a very important point!) Subtracting sequences isn't the same thing as subtracting integers; consider

auto a = std::vector<int>{ 1, 0, 0 };
auto b = std::vector<int>{ 0, 1, 0 };
auto c = (a - b);  // Won't compile!

Likewise, in the meta space,

using a = mpl::vector<mpl::int_<1>, mpl::int_<0>, mpl::int_<0>>;
using b = mpl::vector<mpl::int_<1>, mpl::int_<0>, mpl::int_<0>>;
using c = mpl::minus<a,b>;  // Won't compile!

What we mean to say, in the first case, is

auto c = std::vector<int>{};
std::transform(a.begin(), a.end(), b.begin(), std::back_inserter(c), std::minus<>{});

and what we mean to say, in the second (meta) case, is

using c = mpl::transform<a, b, mpl::minus>::type;  // caveat: we're not done yet

Notice that the C++11 std::transform takes pairs of iterators a.begin(), a.end() instead of just a; it takes b.begin() but not b.end() (a deficiency that is only now being corrected by the Committee); and it mutates c via an output iterator, rather than returning a completely new object, for efficiency. MPL's compile-time meta-version takes containers a and b directly and returns a new container c, i.e., it has value semantics, which IMHO is strictly easier to think about.

So, the above is all correct, EXCEPT for one tiny detail! mpl::transform is actually a very generic algorithm, which means that it expects you to spell out the details of the transformation. You said "mpl::minus", which means "subtract", okay, but subtract what from what? Subtract the elements of the first sequence from elements of the second? Subtract the second's elements from the first? Subtract 42 from the elements of the second sequence and toss out the first one entirely?

Well, we mean "subtract the second sequence's elements, element-wise, from the first's." Which we write as

using c = mpl::transform<a, b, mpl::minus<_1, _2>>::type;

We could equally well write

using c = mpl::transform<b, a, mpl::minus<_2, _1>>::type;

— it would mean the exact same thing.

This generic transform algorithm lets us write complicated transforms such as

// hide some irrelevant boilerplate behind an alias
template<typename... Ts>
using multiplies_t = mpl::multiplies<Ts...>::type;

// compute c = a^2 + 2ab + 1
using c = mpl::transform<a, b,
    mpl::plus<multiplies_t< _1, _1 >,               // a^2 ...
              multiplies_t< mpl::int_<2>, _1, _2 >, // ... + 2ab ...
              mpl::int_<1>>                         // ... + 1
>::type;

Here we can refer to the same element of sequence a three times, using the symbol _1, while _2 refers to the corresponding element of sequence b.

So, that's the point of the symbols _1 and _2 in the context of mpl::transform. But you're probably still wondering how they're implemented. Well, there's no magic here. They might as well be implemented as

template<int> struct _ {};
using _1 = _<1>;
using _2 = _<2>;

As long as they get unique, distinguishable entities in C++'s type system, that's all MPL really cares about.

But in fact they're actually implemented as typedefs for specializations of mpl::arg, which leads to a nifty trick. Since _1 is a synonym for mpl::arg<1>, we can say

_1::apply<A,B,C>::type  is the same type as  A
_2::apply<A,B,C>::type  is the same type as  B
                                ...

and I would guess that mpl::transform is able to take advantage of that fact internally.

like image 128
Quuxplusone Avatar answered Mar 27 '23 10:03

Quuxplusone