Dependent typing in C++
A dependent type is a type that has a dependency on a value. It essentially is way to encode values into the type, that is, every value has a unique type. Non-type template parameters in C++ allow this. Also, std::integral_constant
is good example of dependent typing. Say we want to represent the value 3
we can write:
using three_t = std::integral_constant<int, 3>;
So if we want to pass 3
to another function we could just construct the type, like this:
std::vector<int> v(three_t()); // A vector of size three
Now there is a difference between dependent types and constexpr values, for example:
constexpr void check_positive(int n)
{
static_assert(n > 0, "Value isn't positive"); // Compile error
}
This doesn’t work because int
is not a dependent type even though it is used in constexpr
. We could change it to be a template instead:
template<class N>
constexpr void check_positive(N n)
{
static_assert(n > 0, "Value isn't positive");
}
So this works, if we pass in three_t
to check_positive
:
check_positive(three_t());
Operators
Now, std::integral_constant
is somewhat awkward when you want to write math:
template<class T, class U>
auto sum(T x, U y)
{
return std::integral_constant<int, T::value + U::value>();
}
Instead, lets use the tick::integral_constant
class from Tick. This class extends std::integral_constant
with all the non-mutable operators, so we could write the sum
function like this:
template<class T, class U>
auto sum(T x, U y)
{
return x + y;
}
Which is the same as it would be for any generic sum
functions. So we can call it like this:
template<int N>
using int_ = tick::integral_constant<int, N>;
auto three = sum(int_<1>(), int_<2>());
Or it could be written simply:
auto three = int_<1>() + int_<2>();
What is important is how we can write natural syntax to calculate values at compile-time(without using constexpr
as well, which is not always guaranteed to be at compile-time).
Filtering tuples
Now, say we want to filter a tuple based on a predicate. For example, we may want to only print the values in a tuple that are numbers. We can do that currently using Boost.Fusion with filter_if
like this:
template<class Tuple>
void print_numbers(const Tuple& t)
{
for_each(filter_if<std::is_integral<_>>(t), [](auto x)
{
std::cout << x << std::endl;
});
}
print_numbers(std::make_tuple(1, 2, std::vector<int>()));
Now the std::is_integral<_>
is a placeholder expression. Boost.Fusion uses Boost.MPL library to replace the _
with each type thats in the tuple, and then it evaluates the result. Now, this may not be the worse syntax, but it gets uglier when we want to do something more complicated. For example, we could check if the type is_integral
or is_floating_point
:
template<class Tuple>
void print_numbers(const Tuple& t)
{
for_each(filter_if<or_<std::is_integral<_>, std::is_floating_point<_>>>(t), [](auto x)
{
std::cout << x << std::endl;
});
}
print_numbers(std::make_tuple(1, 2.5, std::vector<int>()));
Now, we can improve this a lot by using depedent typing. Let’s look how to implement an improved filter_if
called simple_filter
, that accepts a C++ function object or lambda for the predicate instead. To understand a little better how filter_if
works, the functions takes either a placeholder expression or a metafunction class. So lets make a metafunction class that will return the result of the function F
called with type T
:
template<class F>
struct predicate
{
template<class T>
struct apply
{
using type = decltype(std::declval<F>()(std::declval<T>());
};
};
With this, we can write the simple_filter
function:
template<class Tuple, class Predicate>
auto simple_filter(Tuple&& t, Predicate)
{
return filter_if<predicate<Predicate>>(std::forward<Tuple>(t));
}
Next, lets wrap the traits so they use tick::integral_constant
instead.
template<class T>
using enhance = tick::integral_constant<typename T::value_type, T::value>;
template<class T>
using is_integral = enhance<std::is_integral<T>>;
template<class T>
using is_floating_point = enhance<std::is_floating_point<T>>;
And finally, we can now write print_numbers
more naturally:
template<class Tuple>
void print_numbers(const Tuple& t)
{
auto numbers = simple_filter(t, [](auto x)
{
return is_integral<decltype(x)>() or is_floating_point<decltype(x)>();
});
for_each(numbers, [](auto x)
{
std::cout << x << std::endl;
});
}
print_numbers(std::make_tuple(1, 2.5, std::vector<int>()));
Although using the placeholder expression is more compact, using a lambda for the predicate looks more “natural”. Finally, after writing simple_filter
, we are able to filter tuples at compile time without needing to write constexpr
nor use template meta-programming.
Of course, this idea is not entirely new. Zach Laine and Matt Calabrese explored similar ideas many years ago at BoostCon in their presentation Instatiations Must Go(slides here). Their original goal was to try to improve performance of metaprogramming by avoiding instantiating templates explicitly, however, in the process they discovered a way to write template metaprogramming using more natural C++ syntax.