Type Transformations

Type Transformations #

Source code: 1uc/modern_numerics_cxx

Given a sequence of types, we’d like a corresponding sequence of transformed types. It’s like map or apply but instead of acting on values it should act on types.

We’ll demonstrate the idea for an array-of-struct (SoA) container. An SoA container rearranges the elements of an array of structs (AoS), e.g.

struct Int_Double {
  int i;
  double d;
};

std::vector<Int_Double> int_doubles;

such that i[i], i[i+1] are stored consecutively (same for d). The goal is to write a data-structure that generalizes,

struct SoA_Int_Double {
  std::vector<int> int_field;
  std::vector<double> double_field;
};

to support the following API:

auto soa = SoA<int, double>(n);
int i5 = soa.get<0>(5);
double d2 = soa.get<1>(2);

Problem: We Can’t Write it Out #

We can’t write one member per template parameter, because we don’t know how many we have. One solution would be to use recursion. However, we’d like to demonstrate how to manipulate types. Therefore, we’ll use existing data structures.

If only we could take T and convert it to std::vector<T> then we could maybe do:

std::tuple<std::vector<T1>, std::vector<T2>, ...>

to store the vectors.

Solution: Template Parameter Packs #

We need a template because we need it to work for different types, we also need a variadic template because we need it to work for arbitrary structs, some maybe have one member others five.

This is simply the syntax for a variadic template. The Args are referred to as a template parameter pack. We’ll be allowed to traverse and apply transformations on template parameter packs, i.e. on a sequence of types.

template <class... Args>
class SoA;

Next we show how the language allows us to apply “functions” to types. The “function” is std::vector<.> and we’d like to apply it to each of the template parameters. The magic is in the .... The jargon is “pack expansion”, see https://en.cppreference.com/w/cpp/language/parameter_pack

What’s happening is that the compiler takes std::vector<Args> and creates a pattern (or “function”) std::vector<.>, next for each T in Args... it’ll substitute the T for the . in the pattern, i.e. where Args use to be. Then it concatenates the expressions, separated by commas.

Example: Tuple of Vectors #

We use pack expansion to have a tuple of vectors:

template <class... Args>
class SoA {
  std::tuple<std::vector<Args>...> data;
};

Example: Constructor #

We need a constructor. It’ll suffice to have a ctor which creates default initialized vectors of the same size. We’re faced with the same problem, we’d like to write

  data{std::vector<int>(n), std::vector<double>(n)}

but more generically. Same solution: std::vector<.>(n) is the “function”/pattern we’d like to apply; and therefore this is the syntax to use:

  SoA(size_t n) : data{std::vector<Args>(n)...} {}

Completing the SoA #

Finally, let’s add an accessor. Since the user is kind enough to tell us the index of the element of the struct they want, we can simply forward that information to std::get:

  template <size_t tuple_index>
  auto& get(size_t vector_index) {
    return std::get<tuple_index>(data)[vector_index];
  }