Typeless Functions

Unlike function templates, parametric expressions do not create a type which means that arguments and return "values" do not require a concrete type either. We will refer to these typeless entities as placeholder expressions. A placeholder expression is an intermediate part of an expression that, by itself, does not refer to an actual value. The names of parameter packs, overloaded functions, as well as the name of parametric expressions themselves are examples of these.

Here is the gratuitous TLDR Tony Table for this post:

std::tuple xs{1, 2, 3, 4, 5};
int sum = std::apply([](auto&& ...x) {
  return (std::forward<decltype(x)>(x) + ...);
}, xs);
std::tuple xs{1, 2, 3, 4, 5};
int sum = (unpack(xs) + ...);

Returning a Parameter Pack

Parameter packs are named declarations that are ultimately expanded into a list of parameters that may be heterogenously typed. The C++ type system has no way to describe a pack so function templates cannot return one without expanding it first. With parametric expressions it becomes possible, and with this, parameter packs become closer to a language level tuple.

template <typename>
struct int_seq_;

template <typename T, T ...i>
struct int_seq_<std::integer_sequence<T, i...>> {
  static using apply() {
    return i;
  }     // ^-- Unexpanded pack
};

Here is a basic example that returns the list of integers in an std::integer_sequence as a parameter pack. Seasoned metaprogrammers will recognize the pattern of using template specialization here to get a pack of indices. In practice this pattern is duplicated everywhere because the computations must occur locally as the packs must be expanded before returning. The above code can be used to replace the pattern and simply allow these packs to be created anywhere. A step further would be to replace the above with an intrinsic that avoids the creation of an std::integer_sequence. For now we can just hide the scary template code with another parametric expression like this:

using int_seq(using auto i) {
  return int_seq_<std::make_index_sequence<i>>::apply();
}

Unpacking a Tuple in Place

Currently the function std::apply can be used to apply computations to all of the values in a tuple. This requires your computations to occur within a function, and unsuprisingly this is what makes lambdas so useful. However, this can still be a bit cumbersome both for the compiler as well as the user. With what we have so far we can implement a replacement quite easily:

using unpack(using auto tuple) {
  return std::get<int_seq(std::tuple_size_v<decltype(tuple)>)>(tuple);
      // ^-- Unexpanded pack
}

Note that what we are returning here is an expression containing an unexpanded parameter pack.

Summary

In simple terms, this post has revealed how parametric expressions may open the door to operations on parameter packs. For the next few posts, I would like demonstrate some of the other well-proven use cases for parametric expressions, but during the course of writing this, something occurred to me. A parameter pack is a compile-time product in terms of algebraic data types. What if we could use a ternary compile-time product to represent a run-time sum type that avoids the intermediate storage properties of an actual sum type like std::variant. This could vastly simplify the recursive visitation pattern I was using in Nbdl or that is typically seen in monadic interfaces.

Thanks for looking at this!

Jason Rice

Follow on Twitter
@JasonRice_

Parametric Expressions

Parametric Expressions are a hygienic macro like language feature proposed for C++.

Check it out! P1221