Metal

Metal is a header-only C++14 library designed to make metaprogramming easy. It provides a powerful high-level abstraction for compile-time algorithms that mimic the Standard Algorithms Library, hence Metal - Metaprogramming Algorithms.

There is a myriad of C++ metaprogramming libraries out there so why Metal?

In a Glimpse

#include <metal.hpp>
// First we need some Values
union x { char payload[10]; };
class y { public: char c; };
struct z { char c; int i; };
// ... from which we construct some Lists
using l0 = metal::list<>;
static_assert(metal::same<l1, metal::list<x>>::value, "");
static_assert(metal::same<l2, metal::list<x, z>>::value, "");
static_assert(metal::same<l3, metal::list<x, y, z>>::value, "");
// Lists are versatile, we can check their sizes...
static_assert(metal::size<l0>::value == 0, "");
static_assert(metal::size<l1>::value == 1, "");
static_assert(metal::size<l2>::value == 2, "");
static_assert(metal::size<l3>::value == 3, "");
// retrieve their elements...
static_assert(metal::same<metal::front<l3>, x>::value, "");
static_assert(metal::same<metal::back<l3>, z>::value, "");
static_assert(metal::same<metal::at<l3, metal::number<1>>, y>::value, "");
// count those that satisfy a predicate...
template<typename T>
template<typename T>
static_assert(metal::count_if<l3, metal::lambda<is_class>>::value == 2, "");
static_assert(metal::count_if<l3, metal::lambda<is_union>>::value == 1, "");
// We can create new Lists by removing elements...
static_assert(metal::same<l0, l0_>::value, "");
static_assert(metal::same<l1, l1_>::value, "");
static_assert(metal::same<l2, l2_>::value, "");
// by reversing the order of elements...
// by transforming the elements...
static_assert(metal::same<l2ptrs, metal::list<x*, z*>>::value, "");
static_assert(metal::same<l3refs, metal::list<x&, y&, z&>>::value, "");
// even by sorting them...
template<typename x, typename y>
static_assert(metal::same<sorted, metal::list<y, z, x>>::value, "");
// that and much more!

Check out more examples below.

Getting Started

Download

There are a few ways to get Metal, the easiest might be to simply download the latest release as a compressed package.

If you have git installed and would rather have the latest stable Metal, you may consider cloning branch master from GitHub.

git clone https://github.com/brunocodutra/metal

Likewise, the bleeding edge development version can be obtained by cloning branch develop instead.

git clone https://github.com/brunocodutra/metal --branch=develop

Install (optional)

Metal may optionally be installed system-wide to ease integration with external projects. If you'd rather use Metal locally, you can skip to the next section.

Make sure to have CMake v3.4 or newer installed on your system, then, from within an empty directory, issue the following commands.

cmake /path/to/Metal
cmake --build . --target install

At this point Metal's include tree will be installed in /usr/local/include on Posix systems and C:\Program Files\Metal\include on Windows.

Integration

If you chose to install Metal system-wide, you just have to make sure the installation prefix is looked up by your compiler.

Using CMake it suffices to add the following to your CMakeLists.txt.

find_package(Metal REQUIRED)
include_directories(${Metal_INCLUDE_DIR})

To use your local copy of Metal instead, just add its include/ sub-directory to the include search paths of your project and you are all set.

Supported Compilers

The following compilers are tested in continuous integration using Travis CI and Appveyor CI.

Compiler Version
GCC ≥ 4.8
Clang ≥ 3.4
Xcode ≥ 6.4
Visual Studio ≥ 14 (2015)
MinGW ≥ 5

Project Organization

Header files are divided in modules named after each concept. Modules are organized in directories and contain algorithms that operate on models of that concept. The complete hierarchy of modules and headers is available on Metal's repository on GitHub.

Tip
You may simply include metal.hpp and get access to all that Metal has to offer without concerning yourself with which specific headers to include.

Concepts

Template metaprogramming may be seen as a language of its own right. It shares the usual syntax of C++ templates, but has unique semantics. Because constructs assume different meanings in its context it is useful to define a few key concepts.

Value

Values are the objects of metaprogramming.

Requirements

Any type is a Value.

Examples

using val = int;
using val = decltype(3.14);
struct val
{
//...
};

Counterexamples

int not_a_val;
decltype(auto) not_a_val = 3.14;
template<typename...>
struct not_a_val
{
//...
};

See Also

metal::value, metal::is_value

Number

A Number is a compile-time representation of a numerical value.

Requirements

num is a model of Number if and only if num is a specialization of metal::number.

Note
metal::number<n> is guaranteed to be an alias template to std::integral_constant<metal::int_, n>.

Examples

using num = metal::false_;
using num = metal::number<-1>;
using num = metal::number<'a'>;

Counterexamples

struct not_a_num :
{};

See Also

metal::number, metal::int_

Expression

Expressions, also called metafunctions, are mappings over the set of Values.

Requirements

expr is a model of Expression if and only if expr is a class, union or alias template that only expects Values as arguments.

Examples

template<typename... vals>
using expr = metal::number<sizeof...(vals)>;
template<typename x, typename y>
struct expr;

Counterexamples

template<template<typename...> class...> // non-type parameter
struct not_an_expr;
template<int v> // non-type parameter
using not_an_expr = metal::number<v>;

Lambda

Lambdas, short for Lambda Expressions, are first-class Expressions. As Values themselves, Lambdas can serve both as argument as well as return value to other Expressions and Lambdas, thus enabling higher-order composition.

Requirements

lbd is a model of Lambda if and only if lbd is a specialization of metal::lambda.

Examples

See Also

metal::lambda, metal::is_lambda

List

A List is a sequence of Values.

Requirements

list is a model of List if and only if list is a specialization of metal::list.

Examples

using l = metal::list<>; // an empty list

See Also

metal::list, metal::is_list

Pair

A Pair is a couple of Values.

Requirements

A Pair is any List whose size is 2.

Examples

See Also

metal::pair, metal::is_pair, metal::first, metal::second

Map

A Map is a collection of unique Values, each of which associated with another Value.

Requirements

A Map is a List of Pairs, whose first elements are all distinct, that is

[[k0, v0], ..., [kn, vn]]; ki != kj for all i, j in {0, n} and i != j

Examples

using m = metal::list<>; // an empty map

Counterexamples

using not_a_map = metal::list< // repeated keys
>;
using not_a_map = metal::list< // not a list of pairs
metal::list<int, int&>
>;

See Also

metal::map, metal::is_map, metal::keys, metal::values

Migrating from Boost.MPL

Metal was heavily influenced by Boost.MPL, from which it inherited the convention of naming algorithms after their counterparts in the C++ standard library. For this reason, metaprograms written using Metal might resemble those written using Boost.MPL, but there are fundamental differences between these libraries that you must keep in mind when porting a legacy metaprogram that uses Boost.MPL to modern C++ using Metal.

Boost.MPL is notable for employing various tricks to emulate features that only became directly supported by the core language much later on with C++11. Most notably, Boost.MPL relies on a template arguments to emulate variadic templates and create an illusion that Sequences, such as mpl::vector or mpl::map, can hold an arbitrary number of elements. However, because these templates could not be truly variadic, every possible size of these Sequences had to be enumerated one by one as a distinct numbered version of the template.

template<typename elm_1>
struct sequence_1 {};
template<typename elm_1, typename elm_2>
struct sequence_2 {};
// ...
template<typename elm_1, typename elm_2, /*...*/ typename elm_n>
struct sequence_n {};

This trick clearly doesn't scale well and implies there must be an upper limit to the size of Sequences. Indeed Boost.MPL limits the sizes of sequences to only a couple of dozen elements by default. Moreover, because this boilerplate is too troublesome to maintain, Boost.MPL relies heavily on the C++ preprocessor, which on one hand reduces code redundancy, but on the other hand dramatically impacts compilation time figures.

Metal has none of these issues, since it takes advantage of variadic templates to reduce that boilerplate to a one-liner, while at the same time overcoming all of the drawbacks mentioned.

template<typename...> struct sequence {};

Indeed, Metal Lists and Maps can easily exceed the hundreds and even thousands of elements with little impact to the compiler performance. For up to date benchmark figures, visit metaben.ch.

Another important difference that arises from the lack of language support at the time Boost.MPL was designed, is the fact that it had no other means of expressing metafunctions other than by the rather verbose idiom of declaring a nested type alias within template classes.

struct na {};
template<typename arg_1 = na, typename arg_2 = na, /*...*/ typename arg_n = na>
struct metafunction
{
typedef result type;
};
template<typename arg_1, typename arg_2, /*...*/ typename arg_n>
struct compound_metafunction
{
typedef typename metafunction<
typename metafunction<arg_1>::type,
typename metafunction<arg_2>::type,
/*...*/
typename metafunction<arg_n>::type
>::type type;
};

Metal on the other hand is able to take advantage of alias templates and make it much less verbose

template<typename... args>
using metafunction = result;
template<typename... args>
using compound_metafunction = metafunction<metafunction<args>...>;

... but that is not all that there's to it. While template aliases produce SFINAE-friendly errors, substitution errors on nested types prevent the SFINAE rule from kicking in and trigger hard compilation errors instead, which is another important drawback of Boost.MPL when compared to Metal. For a discussion about the importance of SFINAE-friendliness, take a look at A Word on SFINAE-Friendliness .

For the reasons discussed, Metal cannot interoperate with Boost.MPL out of the box, but fortunately it is always possible to map Boost.MPL concepts to their equivalents in Metal, such as Sequences to Lists, Metafunction Classes to Lambdas and Integral Constants to Numbers. To ease the migration, Metal provides a built in helper metal::from_mpl that does just that for you, simply include metal/external/mpl.hpp to make it available.

Examples

Tip
In the following examples, IS_SAME(X, Y) is just a terser shorthand for static_assert(std::is_same<X, Y>{}, "").

Parsing Raw Literals


If you ever considered augmenting std::tuple, so that instead of the rather clunky std::get<N>()

static_assert(std::get<1>(std::tuple<int, char, double>{42, 'a', 2.5}) == 'a', "");

one could use the more expressive subscript operator [N]

static_assert(AugmentedTuple<int, char, double>{42, 'a', 2.5}[1] == 'a', "");

you might have come up with something like this

constexpr auto operator [](std::size_t i)
-> std::tuple_element_t<i, std::tuple<T...>>& {
return std::get<i>(*this);
}

only to realize the hard way that this is simply not valid C++14.

error: non-type template argument is not a constant expression

While the keyword constexpr tells the compiler the value returned by operator [] might be a compile time constant, it imposes no such constraint on its arguments, which may as well be unknown at compile-time. It might seem we are out of luck at this point, but let us not forget that long before C++ had constexpr variables, integral constants strictly known at compile time could be expressed with the help of non-type template parameters.

So how about refactoring operator [] to take an instance of metal::number and relying on template pattern matching to extract its non-type template argument?

template<typename... T>
struct AugmentedTuple :
std::tuple<T...>
{
using std::tuple<T...>::tuple;
template<metal::int_ i>
constexpr auto operator [](metal::number<i>)
return std::get<i>(*this);
}
};
static_assert(AugmentedTuple<int, char, double>{42, 'a', 2.5}[metal::number<1>{}] == 'a', "");

That looks promising, but then again metal::number<1>{} is even clunkier than std::get<1>(), we want something more expressive.

A custom literal operator that constructs Numbers out of integer literals could help reducing the verbosity

static_assert(AugmentedTuple<int, char, double>{42, 'a', 2.5}[1_c] == 'a', "");

but how is operator ""_c implemented?

It might be tempting to try something like this

constexpr auto operator ""_c(long long i)
return {};
}

but let us not forget the reason why we got this far down the road to begin with, recall we can't instantiate a template using a non-constexpr variable as argument!

At this point, a watchful reader might argue that in theory there is no real reason for this to be rejected, since the literal value must always be known at compile-time and that makes a lot of sense indeed, but unfortunately that's just not how C++14 works.

All is not lost however, because we can still parse raw literals, in other words, we are in for some fun!

The Raw Literal Operator Template

Raw literal operator templates in C++ are defined as a nullary constexpr function templated over char...

template<char... cs>
constexpr auto operator ""_raw()
-> metal::numbers<cs...> {
return {};
}

where cs... are mapped to the exact characters that make up the literal, including the prefixes 0x and 0b

IS_SAME(decltype(371_raw), metal::numbers<'3', '7', '1'>);
IS_SAME(decltype(0x371_raw), metal::numbers<'0', 'x', '3', '7', '1'>);

as well as digit separators

IS_SAME(decltype(3'7'1_raw), metal::numbers<'3', '\'', '7', '\'', '1'>);

The operator ""_c

We start by defining the literal operator _c as a function that forwards the raw literal characters as a List of Numbers to parse_number and returns a default constructed object of whatever type it aliases to, which in this case is guaranteed to be a Number.

template<char... cs>
constexpr auto operator ""_c()
-> parse_number<metal::numbers<cs...>> {
return {};
}

Resolving the Radix

In its turn parse_number strips the prefix, if any, thus resolving the radix, then forwards the remaining characters to parse_digits, which is in charge of translating the raw characters to the numerical values they represent. The radix and digits are then forwarded to assemble_number, which adds up the individual digits according to the radix.

template<typename tokens>
struct parse_number_impl
{};
template<typename... tokens>
struct parse_number_impl<
metal::list<tokens...>
>
{
using type = assemble_number<
parse_digits<metal::list<tokens...>>
>;
};
template<typename... tokens>
struct parse_number_impl<
metal::list<metal::number<'0'>, tokens...>
>
{
using type = assemble_number<
parse_digits<metal::list<tokens...>>
>;
};
template<typename... tokens>
struct parse_number_impl<
metal::list<metal::number<'0'>, metal::number<'x'>, tokens...>
>
{
using type = assemble_number<
parse_digits<metal::list<tokens...>>
>;
};
template<typename... tokens>
struct parse_number_impl<
metal::list<metal::number<'0'>, metal::number<'X'>, tokens...>
>
{
using type = assemble_number<
parse_digits<metal::list<tokens...>>
>;
};
template<typename... tokens>
struct parse_number_impl<
metal::list<metal::number<'0'>, metal::number<'b'>, tokens...>
>
{
using type = assemble_number<
parse_digits<metal::list<tokens...>>
>;
};
template<typename... tokens>
struct parse_number_impl<
metal::list<metal::number<'0'>, metal::number<'B'>, tokens...>
>
{
using type = assemble_number<
parse_digits<metal::list<tokens...>>
>;
};
template<typename tokens>
using parse_number = typename parse_number_impl<tokens>::type;

Parsing Digits

Before translating characters to their corresponding numerical values, we need to get rid of all digit separators that may be in the way. To do that we'll use metal::remove, which takes a List l and a Value val and returns another List that contains every element in l and in the same order, except for those that are the same as val.

The remaining characters can then be individually parsed with the help of metal::transform, which takes a Lambda lbd and a List l and returns another List that contains the Values produced by the invocation of lbd for each element in l.

[lbd(l[0]), lbd(l[1]), ..., lbd(l[n-2]), lbd(l[n-1])]

Notice how characters are translated to their actual numerical representation.

Thus we have

Assembling Numbers

We now turn to assemble_number. It takes a List of digits and adds them up according to the radix, in other words

D0*radix^(n-1) + D1*radix^(n-2) + ... + D{n-2}*radix + D{n-1}

or, recursively,

((...((0*radix + D0)*radix + D1)*...)*radix + D{n-2})*radix + D{n-1}

This is the equivalent of left folding, or, in the Metal parlance, metal::accumulate, after its run-time counterpart in the standard library.

using radix = metal::number<10>;
using digits = metal::numbers<3, 7, 1>;
template<typename x, typename y>
using lbd = metal::lambda<expr>;
IS_SAME(
metal::accumulate<lbd, metal::number<0>, digits>,
);

Here we introduced a new Expression expr from which we created a Lambda, but we could also have chosen to use bind expressions instead.

using radix = metal::number<10>;
using digits = metal::numbers<3, 7, 1>;
using lbd = metal::bind<
metal::_1
>,
>;
IS_SAME(
metal::accumulate<lbd, metal::number<0>, digits>,
);
Note
If bind expressions look scary to you, don't panic, we will exercise Expression composition in our next example. Here it suffices to keep in mind that bind expressions return anonymous Lambdas, just like std::bind returns anonymous functions, and that metal::_1 and metal::_2 are the equivalents of std::placeholders::_1 and std::placeholder::_2.

Finally

template<typename radix, typename digits>
using assemble_number = metal::accumulate<
metal::lambda<metal::add>,
metal::lambda<metal::mul>,
metal::quote<radix>,
metal::_1
>,
metal::_2
>,
metal::number<0>,
digits
>;

Fun With operator ""_c

IS_SAME(
decltype(01234567_c), //octal
);
IS_SAME(
decltype(123456789_c), //decimal
);
IS_SAME(
decltype(0xABCDEF_c), //hexadecimal
);

It also works for very long binary literals.

IS_SAME(
decltype(0b111101101011011101011010101100101011110001000111000111000111000_c),
);

And ignores digit separators too.

IS_SAME(
decltype(1'2'3'4'5'6'7'8'9_c),
);

Church Booleans


Church Booleans refer to a mathematical framework used to express logical operation in the context of lambda notation, where they have an important theoretical significance. Of less practical importance in C++, even in the context of template metaprogramming, they will nevertheless help us acquaint with bind expressions in this toy example.

The boolean constants true_ and false_ are, by definition, Lambdas that return respectively the first and second argument with which they are invoked.

using true_ = metal::_1;
using false_ = metal::_2;

Now, using the fact that booleans are themselves Lambdas, it's not too hard to realize that invoking a boolean with arguments <false_, true> always yields its negation.

template<typename b>
IS_SAME(not_<true_>, false_);
IS_SAME(not_<false_>, true_);

However, to enable higher-order composition we really need not_ to be a Lambda , not an Expression . Granted one could easily define former in terms of the latter as metal::lambda<not_>, but that would defeat the whole purpose of this exercise, the idea is to use bind expressions directly.

Admittedly a little more verbose, but that saves us from introducing a new named alias template.

Using a similar technique, we can also define operators and_ and or_.

This exercise might me mind-boggling at first, but you'll get used to it soon enough.

Without further ado we present the logical operator xor.

Notice how we bind not_, which is only possible due to the fact it is a Lambda .

A Word on SFINAE-Friendliness


An Expression is said to be SFINAE-friendly when it is carefully designed so as never to prevent the SFINAE rule to be triggered. In general, such Expressions may only trigger template substitution errors at the point of instantiation of the signature of a type, which includes the instantiation of alias templates and default template arguments. SFINAE-friendly Expressions are exceedingly powerful, because they may be used to drive overload resolution, much like std::enable_if does. For this reason, all Expressions in Metal are guaranteed to be SFINAE-friendly.

Conversely, a SFINAE-unfriendly Expression produces so called hard errors, which require the compilation to halt immediately. Examples of hard errors are failed static_assert'ions or template substitution errors at the point of instantiation of the nested members of a type. SFINAE-unfriendly Expressions are very inconvenient, because they force compilation to halt when they are not selected by overload resolution, thereby hindering the usage of the entire overloaded set.

To illustrate how useful SFINAE-friendliness can be, suppose we need a factory function make_array that takes an arbitrary number of arguments and returns a std::array. Because arrays are homogeneous collections, we need the common type of all its arguments, that is, the type to which every argument can be converted to. Fortunately std::common_type_t does just that and is also guaranteed to be SFINAE-friendly as per the C++ Standard.

template<typename... Xs,
typename R = std::array<std::common_type_t<Xs...>, sizeof...(Xs)>
>
constexpr R make_array(Xs&&... xs) {
return R{{std::forward<Xs>(xs)...}};
}

There is one caveat to std::common_type_t however: it doesn't work with std::tuples in general, even though the common tuple is really just a tuple of common types. Hence, we need a new trait that computes the common tuple from a set of tuples so that we may overload make_array.

template<typename... Ts>
using common_tuple_t = metal::apply<
std::common_type_t<metal::lambda<std::tuple>, metal::as_lambda<Ts>...>,
>;
template<typename Head, typename... Tail,
typename R = std::array<
common_tuple_t<std::decay_t<Head>, std::decay_t<Tail>...>,
1 + sizeof...(Tail)
>
>
constexpr R make_array(Head&& head, Tail&&... tail) {
return R{{std::forward<Head>(head), std::forward<Tail>(tail)...}};
}

And it works as expected, for both numerical values

IS_SAME(decltype(make_array(42, 42L, 42LL)), std::array<long long, 3>);

as well as std::tuples

using namespace std::chrono;
using namespace std::literals::chrono_literals;
IS_SAME(
decltype(
make_array(
std::make_tuple(42ns, 0x42, 42.f),
std::make_tuple(42us, 042L, 42.L),
std::make_tuple(42ms, 42LL, 42.0)
)
),
std::array<std::tuple<nanoseconds, long long, long double>, 3>
);

Now, it might not be obvious to the untrained eye, but the reason why overloading works as expected in this example, is precisely the fact common_tuple_t is SFINAE-friendly. If it weren't, as soon as one attempted to call make_array for anything that isn't a std::tuple, the compilation would halt immediately, even if the first overload would be a perfect match otherwise.

To demonstrate this issue, we'll implement the same common tuple trait, but this time using Boost.Hana, which, contrary to Metal, doesn't provide any guarantees regarding SFINAE-friendliness.

template<typename... Ts>
using naive_common_tuple_t = typename decltype(
boost::hana::unpack(
boost::hana::zip_with(
boost::hana::template_<std::common_type_t>,
boost::hana::zip_with(boost::hana::decltype_, std::declval<Ts>())...
),
boost::hana::template_<std::tuple>
)
)::type;

Now, if we use naive_common_tuple_t to overload make_array

template<typename... Xs,
typename R = std::array<std::common_type_t<Xs...>, sizeof...(Xs)>
>
constexpr R make_array(Xs&&... xs) {
return R{{std::forward<Xs>(xs)...}};
}
template<typename Head, typename... Tail,
typename R = std::array<
naive_common_tuple_t<std::decay_t<Head>, std::decay_t<Tail>...>,
1 + sizeof...(Tail)
>
>
constexpr R make_array(Head&& head, Tail&&... tail) {
return R{{std::forward<Head>(head), std::forward<Tail>(tail)...}};
}

it does work as expected for std::tuples

using namespace std::chrono;
using namespace std::literals::chrono_literals;
IS_SAME(
decltype(
make_array(
std::make_tuple(42ns, 0x42, 42.f),
std::make_tuple(42us, 042L, 42.L),
std::make_tuple(42ms, 42LL, 42.0)
)
),
std::array<std::tuple<nanoseconds, long long, long double>, 3>
);

however it produces a compilation error as soon as we try to make an array of anything that is not a Boost.Hana Sequence, even if the first overload remains available and would be a perfect match as we just verified

IS_SAME(decltype(make_array(42, 42L, 42LL)), std::array<long long, 3>);

error: static_assert failed "hana::zip_with(f, xs, ys...) requires 'xs' and 'ys...' to be Sequences"

Frequently Asked Questions

What are some advantages of Metal with respect to Boost.MPL?


The most apparent advantage of Metal with respect to Boost.MPL is the fact Metal Lists and Maps can easily exceed the hundreds and even thousands of elements with little impact to the compiler performance, whereas Boost.MPL Sequences, such as mpl::vector and mpl::map, are hard-limited to at most a couple dozen elements and even then at much longer compilation times and increased memory consumption. Another obvious improvement is the much terser syntax of Metal made possible by alias templates, which were not available at the time Boost.MPL was developed. Finally, Metal is guaranteed to be SFINAE-friendly, whereas no guarantees whatsoever are made with this respect by Boost.MPL.

Visit metaben.ch for up to date benchmarks that compare Metal against Boost.MPL and other notable metaprogramming libraries. For a more detailed discussion on the limitations of Boost.MPL refer to Migrating from Boost.MPL and for a real world example of the importance of SFINAE-friendliness, check out A Word on SFINAE-Friendliness .

What are some advantages of Metal with respect to Boost.Hana?


As a tool specifically designed for type level programming, Metal is able to provide stronger guarantees and much faster compilation times than Boost.Hana when used for similar purposes. In fact, Metal guarantees SFINAE-friendliness, whereas Boost.Hana does not. Check out A Word on SFINAE-Friendliness for a real world example of the limitations of Boost.Hana with this respect.

Moreover, since Metal Concepts are defined by their type signatures, it is always safe to use template pattern matching on them to partially specialize class templates or overload function templates, while the types of most Boost.Hana objects is left unspecified and thus cannot be used for these purposes.

Why isn't std::integral_constant always a Number?


Numbers are defined as a specific specialization of std::integral_constants, whose binary representation is fixed to metal::int_, an implementation-defined integral type. This design choice stems from the fact two Values compare equal if and only if they have the same type signature. As Values themselves, Numbers are also subject to this requirement, thus had Numbers been defined as a numerical value plus its binary representation, would two Numbers only compare equal if they had both the same numerical value and the same binary representation. This is unreasonable in the context of metaprogramming, where the binary representation of numerical values is entirely irrelevant.