Contents

Implementing basic type lists in C++

In this post I’m gonna implement a simple type list along with a set of basic operations that can be performed on it.

Tuples?

Isn’t it just a tuple? Not really, it’s something simpler than that. Tuple is a set of values of arbitrary types. Tuples are immutable as well (there’s std::tuple_cat with which operations like append and prepend could be implemented but this will result with a new instance of tuple with extended set of contents).

In case of typelist, well, it’s just a list of types, without any instances of these types and without any associated values.

What’s the practical aspect of such list? I don’t really want to spoil the next post on that topic, but being able to index the type in a list, it’s possible to implement mappings between indices (or enums) and types.

List type

I’m gonna define a list as a variadic template:

1
2
template <typename ... Types>
struct TypeList{};

Having this, it’s possible to declare lists.

1
2
3
4
5
6

using Integrals = TypeList<uint8_t, uint16_t, uint32_t>;

using RandomTypes = TypeList<int, float, std::string>;

...

Basic operations

Head, Tail, Size and Last are probably the most basic operations you can imagine that can be performed on a list. The first two (Head & Tail) retrieve the first type in the list and the remainder of the list respectively. Size will obviously return number of types within the list and Last returns the last type within the list.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
template <typename List>
struct Size;

template <typename ... T>
struct Size<TypeList<T...>> {
    static constexpr std::size_t value = sizeof...(T);
};

// ===

template <typename List>
struct Head;

template <typename H, typename ... T>
struct Head<TypeList<H, T...>> {
    using type = H;
};

// ===

template <typename List>
struct Tail;

template <typename H, typename ... T>
struct Tail<TypeList<H, T...>> {
    using type = TypeList<T...>;
};

// ===

template <typename List>
struct Last;

template <typename T>
struct Last<TypeList<T>> {
    using type = T;
};

template <typename H, typename ... T>
struct Last<TypeList<H, T...>> {
    using type = typename Last<typename Tail<TypeList<H, T...>>::type>::type;
};

// ===

using List1 = TypeList<int>;
using List2 = TypeList<float, short>;
using List3 = TypeList<int, float, std::string>;

static_assert(std::is_same_v<Head<List1>::type, int>, "");
static_assert(std::is_same_v<Head<List2>::type, float>, "");
static_assert(std::is_same_v<Head<List3>::type, int>, "");

static_assert(Size<List1>::value == 1, "");
static_assert(Size<List2>::value == 2, "");
static_assert(Size<List3>::value == 3, "");

static_assert(std::is_same_v<Last<List1>::type, int>, "");
static_assert(std::is_same_v<Last<List2>::type, short>, "");
static_assert(std::is_same_v<Last<List3>::type, std::string>, "");

The only thing worth explaining here is the Last implementation which employs some recursion. The most basic recursive case is a single element list. Last element in such list is just the first (or the only) element. The recursion reduces the list by removing first element in each step until the list is comprised of a single element only.

List access operations

On top of the basic set of operations, some more convenient ways to access list contents will be necessary as well. That would be Subscript and Index. Subscript will return the type at given index and Index will return the index of the first occurrence of given type within the list.

Subscript

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
template <typename List, std::size_t S>
struct Subscript;

template <typename T, typename ... Types>
struct Subscript<TypeList<T, Types...>, 0> {
	using type = T;
};

template <typename T, typename ... Types, std::size_t S>
struct Subscript<TypeList<T, Types...>, S> {
	using type = typename Subscript<TypeList<Types...>, S - 1>::type;
};

static_assert(std::is_same_v<Subscript<List3, 0>::type, int>, "");
static_assert(std::is_same_v<Subscript<List3, 1>::type, float>, "");
static_assert(std::is_same_v<Subscript<List3, 2>::type, std::string>, "");

Again, recursion to the rescue. Zeroth subscript refers to first type in the list (same as Head), any other one will result in recursive list reduction until it contains only one element.

Index

Index is a bit more difficult, let’s start with the declaration.

1
2
template <typename List, typename T>
struct Index;

Index accepts the type list and the type that we want to find the index of. Recursive base case is when the searched type is the first one on the list.

1
2
3
4
template <typename T, typename ... Types>
struct Index<TypeList<T, Types...>, T> {
    constexpr static std::size_t value = 0;
};

In any other case, similarly as with Subscript or Last the value will have to be recursively incremented and the type list reduced by one element. The code won’t compile should you request a type that is not on the list.

1
2
3
4
template <typename T1, typename T2, typename ... Types>
struct Index<TypeList<T2, Types...>, T1> {
    static constexpr std::size_t value = 1 + Index<TypeList<Types...>, T1>::value;
};

Full implementation below, along with some basic test cases:

1
2
3
4
5
6
using FourInts = TypeList<int, int, int, int>;

static_assert(Index<List1, int>::value == 0, "");
static_assert(Index<List3, float>::value == 1, "");
static_assert(Index<List3, std::string>::value == 2, "");
static_assert(Index<FourInts, int>::value == 0, "");

List modification

To complete the implementation, some routines to extend the list are required. This will be Prepend and Append which add elements to the front or the back of the list respectively. In comparison to prior operations, implementation of these two is very simple.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
template <typename List, typename T>
struct Append;

template <typename T, typename ...Types>
struct Append<TypeList<Types...>, T> {
    using type = TypeList<Types..., T>;
};

template <typename List, typename T>
struct Prepend;

template <typename T, typename ...Types>
struct Prepend<TypeList<Types...>, T> {
    using type = TypeList<T, Types...>;
};

Having implemented these, it’s now possible to extend any list.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
using FiveInts = Append<FourInts, int>::type;

using SixInts = Prepend<FiveInts, int>::type;

using ShortLast = Append<List3, short>::type;

using ShortFirst = Prepend<List3, short>::type;

static_assert(Size<FiveInts>::value == 5, "");
static_assert(Size<SixInts>::value == 6, "");

static_assert(Size<ShortFirst>::value == 4, "");
static_assert(Size<ShortLast>::value == 4, "");
static_assert(std::is_same_v<Head<ShortFirst>::type, short>, "");
static_assert(std::is_same_v<Last<ShortLast>::type, short>, "");

Complete implementation in a form of a library can be found in my gitlab repo. This implementation is a stepping stone which I need to discuss some other concept so, please stay tuned :).