Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/ldionne/mpl11
Enjoy template metaprogramming
https://github.com/ldionne/mpl11
cpp cpp11 cpp14 template-metaprogramming
Last synced: 3 months ago
JSON representation
Enjoy template metaprogramming
- Host: GitHub
- URL: https://github.com/ldionne/mpl11
- Owner: ldionne
- License: bsl-1.0
- Created: 2012-09-08T15:21:15.000Z (over 12 years ago)
- Default Branch: master
- Last Pushed: 2016-12-17T20:56:49.000Z (about 8 years ago)
- Last Synced: 2024-10-26T12:52:23.699Z (3 months ago)
- Topics: cpp, cpp11, cpp14, template-metaprogramming
- Language: C++
- Homepage:
- Size: 5.34 MB
- Stars: 113
- Watchers: 18
- Forks: 10
- Open Issues: 7
-
Metadata Files:
- Readme: README.md
- License: LICENSE.md
Awesome Lists containing this project
README
# MPL11
> Enjoy template metaprogramming## Disclaimers
This is not an official Boost library. It might be proposed as a successor
for the current Boost.MPL in the future, but there is no guarantee. Also, I
am currently working on [Boost.Hana][hana_github], a library which tries to
merge MPL11 and Boost.Fusion into a single library. If that works out, it
would probably replace the MPL11.The library is unstable at the moment; do not use for production.
This was initially supposed to be a simple C++11 reimplementation of the
Boost.MPL. However, for reasons documented in the [rationales](#rationales),
the library was redesigned and the name does not fit so well anymore.## Table of contents
- [Installation](#installation)
- [Introduction](#introduction)
- [Metafunctions](#metafunctions)
+ [Boxed types](#boxed-types)
+ [Laziness](#laziness)
+ [Lifted metafunctions](#lifted-metafunctions)
- [Datatypes and data constructors](#datatypes-and-data-constructors)
+ [Boxed data constructors](#boxed-data-constructors)
+ [Laziness in data constructors](#laziness-in-data-constructors)
+ [Conversions](#conversions)
- [Methods](#methods)
- [Typeclasses](#typeclasses)
- [Rewrite rules](#rewrite-rules)
- [Acknowledgements](#acknowledgements)
- [Rationales](#rationales)
- [Todo list](#todo-list)## Installation
The MPL11 is a header only library. To use it in your own project, just add the
[include](include) directory to your compiler's header search path and you are
done.The library has no dependencies - not even the standard library. However,
it requires a C++14-able compiler. The test suite passes under the following
compilers:
- clang version 3.4
- clang version 3.5.0
- GCC 4.9.0 20140302 (experimental)
- Apple LLVM version 5.1 (clang-503.0.38)To compile the unit tests, you will also need [CMake][]. Once you have it, you
can go to the root of the project and do:```shell
$ mkdir build
$ cd build
$ cmake ..
$ make tests # Compiles the unit tests.
```### Minified version
A minified version of the library is also provided. To use it, simply include
the `boost/mpl11.min.hpp` header, which contains the whole library. Note that
the minified header must not be used in conjunction with other headers from
the library.## Introduction
The MPL11 is a C++11 library providing composable, high-level primitives for
solving complex [template metaprogramming][wiki-tmp] problems. The library is
built around a few core concepts; the aim of this tutorial is to present them,
while the handful of tools provided by the library are left to the
[reference documentation][doxygen-doc].This tutorial assumes a good understanding of template metaprogramming and
basic functional programming concepts. Also, a good understanding of the
[Boost.MPL][] library will be helpful. However, the MPL11 diverges from the
Boost.MPL in many ways, and one should be careful not to transfer knowledge
between both libraries without checking the documentation.## Metafunctions
Informally, a metafunction is a template representing a compile-time function
taking types as arguments and returning a type as a result. Readers coming
from the MPL should be careful here, since the formal definition differs
from that of the MPL.Formally, let `f` be a C++ template with an arbitrary number of type
template parameters, and type template parameters only. Then, `f` is a
__metafunction__ if and only if there exists types `x1, ..., xn` such
that `f::type` is a valid type name. In this context,- `x1, ..., xn` are the __arguments__ of `f`.
- Forming a specialization `f` is called __suspending__ `f`
with `x1, ..., xn`.
- A specialization `f` is called a __thunk__ or a __suspension__.
- The nested `::type` of a thunk is called the __result__ of the thunk. If the
thunk is of the form `f`, we can also say it is the __result__
of `f` with `x1, ..., xn`.
- Fetching the result of a thunk is called __evaluating__ the thunk. If the
thunk is of the form `f`, we can also say __invoking__ `f`
with `x1, ..., xn` or __applying__ `f` to `x1, ..., xn`.
- The __arity__ of a metafunction is the number of arguments it can be
invoked with. A metafunction with arity _n_ is said to be a __n-ary__
metafunction.
- A metafunction that can be invoked with any number of arguments is said
to be __variadic__. By definition, a variadic metafunction is n-ary for
any non-negative integer n.It is important to note the difference between this definition and the one
given by the Boost.MPL. With this definition, a metafunction can never be a
normal C++ type; it must always be a template. Hence, Boost.MPL nullary
metafunctions implemented as non-template classes are not considered as
metafunctions. Here are some examples:```cpp
// A unary metafunction.
template
struct unary { struct type; };// A binary metafunction.
template
struct binary { struct type; };// A variadic metafunction.
template
struct variadic { struct type; };// A nullary metafunction. It can only be invoked with
// 0 arguments, and it is therefore 0-ary (nullary).
template struct nullary;
template <> struct nullary<> { struct type; };// Not a metafunction with the MPL11; it is not a template!
struct MPL_nullary { struct type; };// Not a metafunction; it never has a result (a nested ::type)!
template
struct no_result { };
```### Boxed types
Informally, a boxed type is a type that has yet to be evaluated. Hence,
before knowing the actual "value" of a boxed type, one must evaluate it,
a process which is called unboxing.Formally, for an arbitrary C++ type `T`, a __boxed `T`__ is an arbitrary C++
type `B` such that `B::type` is `T`. In this context, `B` is called a __box__
(of `T`) and fetching the nested `::type` inside of `B` is called __unboxing__
`T`.```cpp
struct T;
struct B { using type = T; }; // a boxed T (equivalently, a box of T)B::type; // unboxing T
```Conversely, wrapping an arbitrary type `T` in a type `B` such that `B::type`
is `T` is called __boxing__ `T` (into `B` or with `B`). It is important to
note that `B` may depend on `T`, without which boxing would be of limited
interest.```cpp
struct T;template
struct B { using type = t; };B; // boxing T into B
```Note that types may be boxed an arbitrary number of times. This is probably
not useful, but the definition is general enough to allow it.```cpp
B>; // this is a "box of B", aka a "box of (box of T)"
```There exists a special boxed type named `undefined` (sometimes called
_bottom_) which has the characteristic of causing a compile-time error
when it is unboxed, even in SFINAE-able contexts. `undefined` can be
seen as an invalid value, or the result of a computation that failed.Here are some examples to illustrate the previous definition:
```cpp
// This template takes an arbitrary type T and boxes it.
template
struct box {
using type = T;
};// These are not boxed.
class x;
struct y { char foo; };
char;
box::type;// These are boxed types.
box; // a boxed `char`
box>; // a boxed `box`
box>::type; // a boxed `char`
struct x { using type = char; }; // a boxed `char`
struct y { struct type; }; // a boxed `y::type`
struct z { using type = z; }; // self-referential? why not!
```It is important to note that there are many different representations for a
boxed `T`. This makes equivalence between boxed types a bit tricky. Consider
the following:```cpp
struct T;
struct B1 { using type = T; }; // a boxed T
struct B2 { using type = T; }; // a boxed T too
```Certainly, `B1` and `B2` are equivalent w.r.t. to the type they box since they
both box the same type `T`. However, `B1` and `B2` are _not_ equivalent w.r.t.
the C++ type system because they are different types. Now, this is important
because it tells us that we can't use pattern matching to define a
metafunction taking a boxed type as an argument. Indeed, since the
representation of a boxed type is not unique, we can't know what form
will have our argument in advance, and therefore we can't pattern match.
Consider the following:```cpp
// B should be a boxed type.
template
struct f;// This should handle boxed chars, but we don't know
// what a boxed char may look like!
template <>
struct f???> {
// ...
};
```Now, we might be tempted to do the following:
```cpp
// box is the template that we defined earlier. It takes an
// arbitrary type and boxes it.
template <>
struct f> {
// ...
};
```But then...
```cpp
template
struct bad {
using type = T;
};// works, as expected
f>::type;// does not work, even though bad is clearly a boxed char
f>::type;
```Instead, we would have to do something more convoluted like:
```cpp
template
struct f_impl;template <>
struct f_impl {
using type = ...;
};template
struct f
: f_impl
{ };f>::type; // works
f>::type; // works too
```> It is interesting to note that boxed types and thunks share a lot. In
> fact, a thunk is nothing but a boxed type that was formed by _suspending_
> a metafunction. Hence, whenever a boxed type is expected, a thunk may be
> used instead.### Laziness
This section introduces the notion of laziness in the context of template
metaprograms and explains how it relates to the previous notions. This is
by no means a rigorous treatment of the broader subject of evaluation
strategies, and knowledge of those concepts is expected.Informally, laziness is a property of a metafunction meaning that the
metafunction performs the least amount of work needed to give its result.
It requires the metafunction to only evaluate the expressions that are
actually needed in its body, and to evaluate them no more than once.The second point is called memoization and it is handled behind the scenes by
the compiler. While the C++ standard does not require compilers to memoize
template instantiations, this is always the case in practice. Consider:```cpp
template
struct f {
using type = x;
};f::type; // instantiate f
f::type; // f is already instantiated, nothing is done.
```The first point, however, must be handled manually when writing template
metaprograms.> __TODO__
> Finish this section. Specifically, explain the following:
>
> - What is a lazy metafunction (mf classes follow from that)
> - The inverse of a lazy metafunction is a strict metafunction (broadly)
> - Lazy metafunctions must take boxed types, otherwise they would always
> evaluate their arguments whether they are needed or not. This is
> equivalent to call-by-name.
> - Strict metafunctions can take unboxed arguments, because they always
> evaluate their arguments anyways. However, strict metafunctions can still
> take boxed arguments and unbox them unconditionally; this is just a matter
> of convention.
> - Metafunctions take boxed arguments by default in the MPL11.
> - Metafunctions are lazy by default (i.e. whenever possible) in the MPL11.
> - Strict metafunctions usually still take boxed arguments for consistency.
> - Some metafunctions don't follow the convention, and in this case this
> behavior is documented.
> - Metafunctions that don't follow the convention do it because it's
> necessary or because it's much easier to do such or such when breaking
> the convention.
> - Why are lazy metafunctions useful?
> This could use the `drop` metafunction of the old introduction.
> Laziness often leads to increased expressiveness; for example it becomes
> easy to define new control structures and infinite data structures.
> - Consider keeping the optional section on non-strictness.At this point, it is probably helpful to clarify that returning from a lazy
metafunction is no different than returning from a strict metafunction. For
example, consider the following lazy metafunction implementing an `if`
statement:```cpp
template
struct if_ {
using Branch = std::conditional::type;
using type = typename Branch::type;
};
```Since `if_` is lazy, its arguments are all boxed types. Here, we unbox
`Branch` and return that instead of returning `Branch` itself. This way,
`if_` is a thunk and we can pass it to other lazy
metafunctions as-is:```cpp
// A lazy metafunction.
template
struct f;using result = f< if_ >::type;
```If we had defined `if_` as follows
```cpp
template
struct if_ {
using Branch = std::conditional::type;
using type = Branch; // Note that we don't unbox Branch here
};
```then `if_` would return a thunk and we would need to do the following instead:
```cpp
using result = f< if_::type >::type;
^^^^^^
```### Lifted metafunctions
Informally, a lifted metafunction is a representation of a metafunction that
allows it to be manipulated as a first class citizen in template metaprograms.
Formally, an arbitrary C++ type `f` is a __lifted metafunction__ if and only
if `f::apply` is a metafunction. In general, lifted metafunctions inherit the
terminology of metafunctions, and the characteristics of a lifted metafunction
follow from that of its nested `apply` metafunction. For example, the arity of
a lifted metafunction `f` is that of `f::apply`.> The definition of a lifted metafunction is almost the same as the definition
> of a metafunction class in the Boost.MPL. The only difference is the
> difference between metafunctions in both libraries.Here are some examples of lifted metafunctions:
```cpp
// A unary lifted metafunction.
struct unary {
template
struct apply { struct type; };
};// A binary lifted metafunction.
struct binary {
template
struct apply { struct type; };
};// A variadic lifted metafunction.
struct variadic {
template
struct variadic { struct type; };
};// A nullary lifted metafunction.
struct nullary {
template struct apply;
};
template <> struct nullary::apply<> { struct type; };// Not a lifted metafunction with the MPL11; its nested apply
// is not a metafunction!
struct MPL_nullary {
struct apply { struct type; };
};// Not a lifted metafunction; its nested apply never has a result!
struct no_result {
template
struct apply { };
};
```Any metafunction can be lifted, and the MPL11 defines a template to do
just that.```cpp
template class f>
struct lift {
template
struct apply
: f
{ };
};
```> `lift` is essentially the same as `quote` in the Boost.MPL. The name
> `lift` was preferred because of its relation to a _lift_ in category
> theory. For completeness, `lift` is actually an equivalence of categories
> between the category where objects are C++ types and morphisms are
> templates to the category where objects are C++ types and morphisms
> are structs with a nested `apply` template.## Datatypes and data constructors
At compile-time, a type becomes a value. A legitimate question would then be:
what is the type of that value? In the MPL11, datatypes play that role.
Closely related are data constructors, which are a way of creating values
of a given datatype. For example, let's create a simple compile-time list:```cpp
// A "tag" representing the datatype.
struct List;// The list constructor. It represents a value of type List that
// contains the provided elements.
template
struct list { using mpl_datatype = List; };// The cons constructor. It represents a value of type List
// with the provided head and tail.
template
struct cons { using mpl_datatype = List; };// The nil constructor. It represents an empty List.
struct nil { using mpl_datatype = List; };
```Data constructors must provide a nested `::mpl_datatype` alias representing
their datatype. One can then use the `datatype` metafunction to retrieve it:```cpp
datatype::type; // == List
```It is also possible to specialize `datatype` instead of providing a
nested `mpl_datatype` alias. So this definition of `cons` is equally
valid (and the other constructors could be defined analogously):```cpp
template
struct cons; // no nested mpl_datatypenamespace boost { namespace mpl11 {
template
struct datatype> {
using type = List;
};
}}
```Being able to do this is paramount when adapting code over which we don't have
the control, but for simplicity we'll stick with the nested `mpl_datatype`
whenever possible. When unspecialized, `datatype` simply returns
`T::mpl_datatype` if that expression is a valid type name, and `Foreign`
otherwise. Hence, `Foreign` is the default datatype of everything.> Note that `datatype` is a strict metafunction and that it does not obey the
> convention of taking boxed arguments. Breaking the convention is necessary
> to allow user-defined specializations.### Boxed data constructors
While it is not mandatory, it is often a good idea to box data constructors
since it makes them usable as-is in lazy metafunctions. Let's rewrite the
previous data constructors that way:```cpp
template
struct list_impl {
using mpl_datatype = List;
};template
struct cons_impl {
using mpl_datatype = List;
};struct nil_impl { using mpl_datatype = List; };
template
struct list {
using type = list_impl;
};template
struct cons {
using type = cons_impl;
};struct nil {
using type = nil_impl;
};
```The downside is that we end up with twice the amount of code, half of which
is complete boilerplate. Also, this approach causes twice as many templates
to be instantiated each time we unbox a data constructor, which can hurt
compile-time performance. Fortunately, we can use self-referential boxing
to make this better.```cpp
template
struct list {
using type = list;
using mpl_datatype = List;
};template
struct cons {
using type = cons;
using mpl_datatype = List;
};struct nil {
using type = nil;
using mpl_datatype = List;
};
```That way, only one additional line of code is required per data constructor
and we only instantiate one template when we unbox it. Indeed,
`cons<...>::type` is just `cons<...>`, which is already instantiated.You might wonder why I have even bothered with the inferior solution using
`cons_impl` and friends in the first place, since the self-referential
solution is so obvious. This was to highlight the fact that boxed data
constructors do not _need_ to be self-referential; it is merely a convenient
implementation trick. This is a subtle difference between the MPL11 and the
[mpllibs][] library collection, which I wanted to point out.### Laziness in data constructors
There is something rather important that we have left undefined when we
created the `list` and `cons` data constructors: what do their arguments
look like?```cpp
template
struct list;template
struct cons;
```There are two possible answers here, and both are valid. In the end, this is
mainly a design choice. The first option is to require the arguments to be
normal (non-boxed) types. We could then use the constructors like so:```cpp
using x = list;
using y = cons>;
using z = cons>>;
```The second option is to require boxed arguments. We could then use the
constructors like so:```cpp
using x = list, box, box>;
using y = cons, list, box>>;
using z = cons, cons, cons, nil>>>;
```> Note that we do not need to box the second arguments to `cons` ourselves,
> because we have already made `list`, `cons` and `nil` boxed.This is clearly less natural than the first solution. Still, for reasons
that will soon become clearer, the MPL11 `List` constructors use the second
solution, and so will we for the rest of this tutorial. To reduce the
syntactic noise, we will define aliases that will make our life easier:```cpp
template
using list_ = list...>;template
using cons_ = cons, box>;
```> Note that we would not need to box the second argument of `cons_` because we
> expect it to be a `List`, and all `List` constructors are already boxed. So
> `box` is really redundant, but we still do it here for the sake of
> clarity.We will say that a data constructor taking unboxed arguments is __strict__,
and that a data constructor taking boxed arguments is __lazy__. An interesting
observation is that some (but not all) constructors are also metafunctions.
Specifically, boxed constructors taking type template parameters are
metafunctions. Therefore, strict boxed constructors correspond to strict
metafunctions, and lazy boxed constructors correspond to lazy metafunctions!### Conversions
The MPL11 provides a way to convert values from one datatype to the other. The
usefulness of this is clearest when implementing numeric datatypes, but we'll
stick with `List` because we already have it. Let's suppose we want to convert
values from a `List` to an `std::tuple` and vice-versa. First, we will need to
define a datatype of which `std::tuple` can be a data constructor.```cpp
struct StdTuple;namespace boost { namespace mpl11 {
template
struct datatype> {
using type = StdTuple;
};
}}
```We can now consider `std::tuple` as a data constructor for the `StdTuple`
datatype. Note that unlike the arguments to `list`, the arguments to
`std::tuple` must be unboxed; this will be important for what follows.
The next step is to implement the conversion itself. This is done by
specializing the `cast` lifted metafunction for the involved datatypes.```cpp
namespace boost { namespace mpl11 {
template <>
struct cast*from*/ List, /*to*/ StdTuple> {
template
struct apply;template
struct apply> {
using type = std::tuple;
};template <>
struct apply {
using type = std::tuple<>;
};template
struct apply> {
using type = /* omitted for simplicity */;
};
};template <>
struct cast*from*/ StdTuple, /*to*/ List> {
template
struct apply;template
struct apply> {
using type = list_;
};
};
}}
```There are a few things to note here. First, it is very important to provide a
conversion for all the data constructors of a datatype. If, for instance, we
had forgotten to provide `apply`, we could only convert from the `list`
and `cons` constructors. Second, we had to unbox the elements of the `list`
when passing them to `std::tuple` because `std::tuple` expects unboxed types.
Similarly, we had to box the elements of the `std::tuple` when passing them to
`list`. Third, the `cast` lifted metafunction is strict and it does not follow
the convention of taking boxed arguments, which makes it possible to pattern
match data constructors. Here is how we could now convert between the two
datatypes:```cpp
using my_list = list_;
using my_tuple = std::tuple;cast::apply::type; // == my_tuple
cast::apply::type; // == my_list
```Also note that casting from a datatype `T` to itself is a noop, so you don't
have to worry about that trivial case. The library also defines a handy
`cast_to` lifted metafunction that reduces the syntactic noise of `cast`:```cpp
cast_to::apply::type; // == my_tuple
cast_to::apply::type; // == my_list
````cast_to` simply deduces the datatype to cast from by using that of its
argument and then forwards to `cast`. `cast_to` is strict and does not
take boxed arguments.> __TODO__: Give more details on the `Foreign` datatype.
## Methods
So far, we have created the `List` datatype and a couple of constructors to
create "values" of that type, but we still don't have a way to manipulate
those values in a useful way. Let's define a `head` metafunction that will
return the first element of a `List`. We will stick to the convention of
taking boxed arguments.```cpp
template
struct head_impl;template
struct head_impl>
: Head
{ };template
struct head_impl>
: Head
{ };// head_impl is not defined since that's an error.
template
struct head
: head_impl
{ };
```First, we need to add a level of indirection (`head_impl`) because `head`
receives boxed arguments and we need to pattern match the constructors.
Second, note that `head` is a strict metafunction because its argument
is always evaluated. Third, we inherit from `Head` in `head_impl` because
the `List` constructors are lazy and hence `Head` is boxed.It is now possible to see why it was useful to make the `List` constructors
lazy. Consider the following situation:```cpp
using refs = list<
std::add_lvalue_reference,
std::add_lvalue_reference
>;head::type; // == int&
```Since we can't form a reference to `void`, we will trigger a compile-time
error if we evaluate the `std::add_lvalue_reference` thunk. However,
since we only ever use the value of the first element in the list, it would
be nice if we could only evaluate that element, right? Making `list` a lazy
constructor makes that possible. If, instead, we had decided to make `list`
strict, we would need to write:```cpp
using refs = list<
std::add_lvalue_reference::type,
std::add_lvalue_reference::type
>;
```which does not compile. The same reasoning is valid if the contents of the
list were the results of complicated computations. By making the constructor
lazy, we would only need to evaluate those computation whose result is
actually used. In the end, the reasons for writing lazy data constructors
are similar to the reasons for writing lazy metafunctions.The `head` metafunction we have so far is useful, but consider the following
datatype and lazy boxed constructor:```cpp
struct Vector;template
struct vector {
struct type {
using mpl_datatype = Vector;
};
};template
struct vector {
struct type {
using head = Head;
using mpl_datatype = Vector;
};
};
```Since `Vector` is really some kind of `List`, it is only reasonable to expect
that we can invoke `head` on a `Vector` just like we do on a `List`. But how
would we go about implementing `head` for `Vector`? Assuming we can't modify
the implementation of `Vector`, the only way we have right now is to use
partial specialization of `head_impl` on `Vector`'s constructor.
Unfortunately, that constructor is `vector<...>::type`, not `vector<...>`.
Since we can't partially specialize on a dependent type, we're out of luck.
To bypass this limitation, we will refine `head` so it uses the datatype of
its argument.```cpp
template
struct head_impl;template
struct head
: head_impl::type>::
template apply
{ };
```We now consider `head_impl` as a lifted metafunction parameterized over the
datatype of its argument. Implementing `head` for `List` and `Vector` is now
a breeze.```cpp
template <>
struct head_impl {
template struct apply;template
struct apply>
: Head
{ };template
struct apply>
: Head
{ };
};template <>
struct head_impl {
template
struct apply
: v::head
{ };
};
```It is sometimes useful to exert a finer grained control over template
specializations than what we currently have. A common idiom is for the
primary template to provide an additional dummy parameter which can be
SFINAE'd upon in partial specializations:```cpp
template
struct trait;template
struct trait::value>> {
// ...
};
```This enables the specialization to depend on an arbitrary compile-time boolean
expression (in fact on the syntactic validity of an arbitrary expression).
Note that all partial specializations using the enabler must have mutually
exclusive conditions or the specialization will be ambiguous; this can be
tricky at times. A variant of this trick is to use a default type of `true_`
instead of `void` for the dummy template parameter. That makes it possible to
leave the `std::enable_if_t` part for most use cases:```cpp
template
struct trait;template
struct trait::value>> {
// ...
};
```> Note that we used `bool_<...>` instead of `std::is_trivial::type` because
> the latter is `std::true_type`, which is not necessarily the same as `true_`.
>
> Also, we don't use a straight `bool` for the enabler because partial
> specializations cannot have dependent non-type template arguments.Since this functionality can really be useful, it might be a good idea to
support it in the `head_impl` lifted metafunction. Fortunately, that only
requires changing the `head_impl` forward declaration to:```cpp
template
struct head_impl;
```> __TODO__: Provide a use case where this is useful.
In this section, we went through the process of designing a simple yet
powerful way of dispatching metafunctions. The subset of metafunctions
using this dispatching technique are called __methods__ in the MPL11.> __TODO__: Tackle binary operators and mixed-datatype dispatch.
### Typeclasses
Typeclasses come from the observation that some methods are naturally related
to each other. For example, take the `head`, `tail` and `is_empty` methods.
When implementing any of these three methods, it is probable that the other
two should also be implemented. Hence, it would be logical to group them in
some way; that is the job of typeclasses. However, typeclasses are much more
than mere method bundles. They provide a clean way of specifying and extending
the interface of a datatype through a process called typeclass instantiation.Let's make a typeclass out of the `head`, `tail` and `is_empty` methods.
Datatypes supporting all three methods look somewhat like `List`s; hence
we will call the typeclass `Iterable`. We start by grouping the `*_impl`
lifted metafunctions of the methods together under the `Iterable` banner.
In the following code snippets, the `tail` and `is_empty` methods will be
omitted when illustrating `head` suffices.```cpp
struct Iterable {
template
struct head_impl;// ...
};template
struct head
: Iterable::head_impl::type>::
template apply
{ };// ...
```To implement the methods for `List`, we now have to write:
```cpp
template <>
struct Iterable::head_impl {
template
struct apply {
// ...
};
};
```Soon enough, we notice that we can regroup the datatype parametrization on
`Iterable` instead of leaving it on each nested lifted metafunction.```cpp
template
struct Iterable {
struct head_impl;// ...
};template
struct head
: Iterable::type>::
head_impl::template apply
{ };// ...
```The next logical step is to prune the superfluous indirection through the
`*_impl` lifted metafunctions and to simply make them metafunctions.```cpp
template
struct Iterable {
template
struct head_impl;// ...
};template
struct head
: Iterable::type>::
template head_impl
{ };// ...
```Since it might be useful to query whether a datatype supports the operations
of `Iterable`, we would like to have a boolean metafunction that does just
that. Fortunately, we can use the `Iterable` for this task with a small
modification.```cpp
template
struct Iterable : false_ {
// ...
};
```By default, `Iterable` is therefore also a boolean metafunction returning
`false`, meaning that arbitrary datatypes don't implement the `head`, `tail`
and `is_empty` metafunctions. In its current form, the `Iterable` template is
called a __typeclass__, and metafunctions like `head` following this
dispatching pattern through a typeclass are called __typeclass methods__.
In order to implement `head` and friends, one would now write```cpp
template <>
struct Iterable : true_ {
template
struct head_impl {
// ...
};// ...
};static_assert(Iterable{}, "List is an Iterable!");
```The three methods `Iterable` contains so far are very basic; for any given
datatype, it is not possible to provide a suitable default implementation.
However, there are other metafunctions that can be implemented in terms of
these three basic blocks. For example, consider the `last` metafunction that
returns the last element of an `Iterable`. A possible implementation would be:```cpp
template
struct last
: if_>,
head,
last>
>
{ };
```While we could provide this metafunction as-is, some datatypes might be able
to provide a more efficient implementation. Therefore, we would like to make
it a method, but one that can be defaulted to the above.> Note that method dispatching incurs some compile-time overhead; hence there
> is a tradeoff between using (typeclass) methods and regular metafunctions.
> The rule of thumb is that if a method is likely to never be specialized
> (i.e. the default implementation is almost always the best), then it should
> probably be a regular metafunction.It turns out that providing a default implementation can be done easily using
typeclasses and a little convention. First, we make `last` a normal typeclass
method.```cpp
template
struct last
: Iterable::type>::
template last_impl
{ };
```Then, we require specializations of `Iterable` to inherit some special type
as follows:```cpp
template <>
struct Iterable : instantiate::with {
// ...
};
```Here, `instantiate<...>::with<...>` is `true_` by default. Hence, it only
takes care of making `Iterable` a true-valued boolean metafunction, which we
did by ourselves previously. However, `instantiate` may be specialized by
typeclass designers in such a way that the member template `with` also
contains default methods. In our case, we would provide a `last_impl`
metafunction corresponding to the default implementation of `last` shown
above. This way, if a datatype does not implement the `last` method, our
default implementation will be used.```cpp
namespace boost { namespace mpl11 {
template <>
struct instantiate {
template
struct with : true_ {
template
struct last_impl {
// default implementation
};
};
};
}}
```This technique provides a lot more flexibility. One notable improvement is the
ability to add new methods to `Iterable` without breaking existing client code,
provided the new methods have a default implementation. Hence, in the MPL11,
all typeclass specializations are required to use this technique, regardless
of whether they actually need defaulted methods.> You might wonder why we use `instantiate::with`
> instead of just using `instantiate`. This is because some
> typeclasses need to know the datatype(s) they operate on to provide
> meaningful defaults. Also, we don't use `instantiate`
> because that would make defaulted typeclass parameters hard to handle.A typeclass specialization using the technique we just saw is called a
__typeclass instantiation__. When a typeclass instantiation exists for
a typeclass `T` and a datatype `D`, we say that `D` is an __instance__ of
`T`. Equivalently, we say that `D` __instantiates__ `T`, or sometimes that
`D` __is a__ `T`. The set of definitions that _must_ be provided for a
typeclass to be complete is called the __minimal complete definition__ of
the typeclass. The minimal complete definition is typically the set of methods
without a default implementation, but it must be documented for each typeclass.> The term _typeclass instantiation_ is borrowed from Haskell and should not be
> mistaken with _template instantiation_ even though they share similarities,
> especially in the MPL11.> __TODO__
> - Tackle mixed-datatype typeclass-method dispatch### Rewrite rules
__TODO__## Acknowledgements
The development of this library drew inspiration from a couple of projects with
different levels of involvement in template metaprogramming. I would like to
thank the people involved in these projects for their work, without which this
library wouldn't be the same. The most notable sources of inspiration and
enlightenment were:- The [Haskell][] programming language
- The original [Boost.MPL][]
- The [mpllibs][] library## Rationales
This section contains rationales for some design decisions of the library. In
its early development, the library was rewritten several times because
fundamental aspects of it needed to be changed. Hence, only the rationales
pertaining to the current design are kept here. If you have a question about
a design decision that is not explained here, please contact the creator of
the library (Louis Dionne).### Why are iterators not used in the library?
The following points led to their removal:
- Lazy views were hard to implement because they required creating new
iterators, which is a pain. Using a different abstraction for sequences
made it much easier.
- Iterators being hard to implement and non-composable is a known problem,
which is addressed e.g. by the Boost.Range library or in this
[paper][on-iteration] by Andrei Alexandrescu.
- There is no performance gain to be achieved by using iterators. In fact, it
often makes straightforward implementations more complicated, which can hide
possible optimizations.
- Implementing infinite ranges using iterators is hacky at best.### Why isn't `apply` a method?
There are two main reasons for this. First, if `apply` were a method, one would
need to make every lifted metafunction an instance of the typeclass defining
`apply`. Since lifted metafunctions are very common, that would be very
cumbersome. Second, making `apply` a method requires using the usual method
dispatching mechanism, which adds some overhead.### Why aren't `and_`, `or_` and `not_` methods?
In some previous design of the library, these were methods. However, allowing
`and_` and `or_` to be non-strict required special casing these methods. Since
I could not find any use case, I decided to make them normal metafunctions.### Why aren't `*_t` versions of metafunctions provided like in C++1y?
Switching the evaluation burden from the caller to the callee makes this useless
in most cases.### Why are MPL-style non-template nullary metafunctions not allowed?
It introduces a special case in the definition of metafunction that prevents us
from using `f::apply<>` to invoke a nullary lifted metafunction. We have to use
`apply`, which will then use either `f::apply<>` or `f::apply`. This adds a
template instantiation and an overload resolution to each lifted metafunction
invocation, which significantly slows down compilation. Considering nullary
metafunctions are of limited use anyway (why would you want a function without
arguments in a purely functional setting?), this is not worth the trouble. Also,
note that MPL-style nullary non-template metafunctions fit in the definition of
a boxed type, so they're not completely forgotten.## Todo list
- [ ] What should we do for default typeclass methods?
1. We reuse the "official" method and we rebox the arguments.
```cpp
template
using equal_impl = not_, box>>;
```2. We create an "official" unboxed method and we use that.
```cpp
template
using equal_impl = not_>;
```
where `not_equal_` is a non-boxed version of `not_equal`.3. We directly forward to the implementation of the method in the
typeclass.
```cpp
template
using equal_impl = not_::not_equal_impl>;
```
- [ ] Implement cross-type typeclasses.
- [ ] Implement associative data structures.
- [ ] Implement a small DSL to implement inline lifted metafunctions (like
Boost.MPL's lambda). Consider let expressions. Using the Boost.MPL lingo,
such a DSL should:
- Allow leaving placeholders as-is inside a lambda, if this is desirable.
- Allow performing placeholder substitution in a lambda without actually
evaluating the expression, if this is desirable.
- Allow "variadic placeholders", i.e. placeholders expanding to several
types. One pitfall of this is using such a placeholder with a
metafunction that is not variadic:```cpp
template
struct f;
using F = lambda>; // error here, f is not unary
using Result = F::apply::type;
```
This fails because `f` requires 2 arguments.
- [ ] Consider allowing types to be printed somehow. The idea is to have
something like a `Show` typeclass that allows types to be pretty-printed
for debugging purposes.
- [ ] Think about a convention or a system to customize some metafunction calls.
Something neat would be to have a way of passing a custom predicate when
comparing sequences; that would make `equal` as powerful as the `equal`
algorithm from the Boost.MPL. Maybe we can achieve the same effect in
another way.
- [ ] Consider having a wrapper that allows treating template specializations
as data. Something like sequence operations on template specializations
and/or tree operations.
- [ ] Consider adding `while_` and `until` metafunctions.
- [ ] Consider ditching `Foreign` and making the default datatype the data
constructor itself.
- [ ] Consider adding `Either`.
- [ ] Right now, we must include `boost/mpl11/core.hpp` to get the
`instantiate<>` template in client code. Maybe typeclass headers
should take care of it. Or maybe `boost/mpl11/core.hpp` should
never have to be included by clients altogether?
- [ ] Add interoperability with the Boost.MPL, Boost.Fusion and components
in the `std` namespace.
- [ ] Use `constexpr` to perform numeric computations on homogeneous sequences
of integral constants.
- [ ] Consider providing data constructors taking unboxed types for convenience.
- [ ] Consider making `int_<>` a simple boxed `int` without a value.
- [ ] Write a rationale for why we don't have parameterized datatypes.
Or is this possible/useful?
- [ ] Make bool.hpp lighter. In particular, it should probably not depend
on integer.
- [ ] Design a StaticConstant concept?
- [ ] In the tutorial, when we specialize lifted metafunctions inside the
`boost::mpl11` namespace, we don't make them boxed. This makes sense
because they are lifted metafunctions, not boxed lifted metafunctions.
What should we do? Should we
1. Make the specializations boxed
2. Do not take for granted that they are boxed when we use them in the
library and box them redundantly, which is correct but suboptimal.
3. Explicitly state that nothing may be specialized inside the
`boost::mpl11` namespace unless specified otherwise. Then,
specify what can be specialized on a per-component basis and
then we apply (2) only to the components that might not be boxed.
- [ ] Consider having a public data constructor for `Foreign`, which would
simply preserve type identity. Also; might consider changing the name
of `Foreign`.
- [ ] Consider regrouping the typeclass instantiations of a datatype in the
datatype itself. This was done in a previous version, but it might have
some advantages.
- [ ] Consider providing automatic currying for metafunctions.
- [ ] By making some std algorithms constexpr and providing a couple of
containers with constexpr iterators, would we have constexpr for free
almost everywhere?
- [ ] Consider making `bool_` a lifted metafunction that behaves like `if_`.
- [ ] Provide a better syntax for casting. Consider `cast`.
- [ ] Seriously consider making datatypes lifted metafunctions.
- [ ] Consider prototype-based objects?[Boost.MPL]: http://www.boost.org/doc/libs/1_55_0b1/libs/mpl/doc/index.html
[call-by-value]: http://en.wikipedia.org/wiki/Call_by_value
[Cmake]: http://www.cmake.org
[doxygen-doc]: http://ldionne.github.io/mpl11
[hana_github]: http://github.com/ldionne/hana
[haskell-denot-semantics]: http://en.wikibooks.org/wiki/Haskell/Denotational_semantics
[Haskell]: http://www.haskell.org
[lazy]: http://en.wikipedia.org/wiki/Lazy_evaluation
[mpllibs]: http://github.com/sabel83/mpllibs
[on-iteration]: http://www.informit.com/articles/article.aspx?p=1407357
[wiki-tmp]: http://en.wikipedia.org/wiki/Template_metaprogramming