A C++ type list, but how and why
Since C++-11, C++ knows variadic templates. Templates that take a parameter pack as an argument. A template parameter pack is a template parameter that accepts zero or more template arguments.
We know how to use them. Standard types like std::tuple
or std::variant
are examples most developers are very familiar with.
- Example
-
std::variant<int8_t, int16_t, int32_t, float>
A type list
Let us focus on the int8_t, int16_t, int32_t, float
part.
This is, obviously, a list of types.
Now the question is, could we do anything with such a list of types, and why would that be useful?
A possible use case
Say we have a sender that sends data to a receiver. The sender can send different types of data, but we want to restrict the types of data that can be sent.
Reusing the type list above, we allow the sender to send signed numbers, but not larger than 32 bits.
Sender<int8_t, int16_t, int32_t, float> send32;
send32.send(42); // OK if int is <= 32 bits,
// otherwise compile error
send32.send(42.0f); // OK, float is 32
send32.send(42L); // maybe OK if long is 32 bits
// compile error if not
send32.send(42LL); // -> compile error
send32.send(42.0); // -> compile error
This can give us some safety guarantees, regardless of our platform.
Implementation
We have a sender with a type list.
All we need to do is a templated send function that checks if the type is in the list.
So let’s to that!
A minimal type list looks like this.
template<typename... Types>
struct typelist {
};
That alone is not particular useful. But we can add a function that checks if a type is in the list.
template<typename... Types>
struct typelist {
template<typename T>
static constexpr bool includes() {
return std::disjunction<std::is_same<T, Types>...>::value;
}
};
That’s it. Simple! Now add some tests.
int main() {
// list accepted types
using num32 = typelist<int8_t, int16_t, int32_t, float>;
// ok types
static_assert(num32::includes<int16_t>());
static_assert(num32::includes<int32_t>());
static_assert(num32::includes<float>());
// unwatned types
static_assert(!num32::includes<double>());
static_assert(!num32::includes<uint16_t>());
static_assert(!num32::includes<long>());
}
You can play around with this code on https://godbolt.org/z/d3EnT6Pdc
What about the sender?
Well, I leave that as an exercise for the reader. 😉
Just make sender that has a void send(auto val)
function that checks if the type of val
is in the list of accepted types.
Please share you solution in the comments!
Programming with types
Let me add another function to show some more compile time programming based on types.
The type list shall be unique
We want a compile time function that checks if all types in the list are unique.
Therefore, we need a function that
-
Takes the first element and the rest (head and tail)
-
Checks if the head is in the tail
-
If it is, return false, the list is not unique
-
If not, call the function recursively with the tail
-
This will look at the first element of the tail as the head
-
Continue until the tail is empty
-
Once the tail is empty, return true, the list is unique
Put it together as code
template<typename... Types>
struct typelist {
static consteval bool unique()
{
auto unroll = [](auto head, auto... tail) {
if constexpr (sizeof...(tail) == 0) {
return true;
} else {
using tail_list = typelist<decltype(tail)...>;
auto head_in_tail = tail_list::template includes<decltype(head)>();
auto tail_unique = typelist<decltype(tail)...>::unique();
return !head_in_tail && tail_unique;
}
};
return unroll(Types{}...);
}
template<typename T>
static consteval bool includes() {
return std::disjunction<std::is_same<T, Types>...>::value;
}
};
Checking if the head is in the tail is done by using a type list.
We simply use the includes
function we already have.
The syntax becomes a bit 'special', but that’s how it is in C++.
Compile time programming
Compile-time programming often looks like functional programming.
In this example, instead of a loop, what a C++ developer would expect, we use recursion to loop over the list. This might change in future since compile time programming becomes more and more relaxed. C++-11 had much stricter rules about what can be done at compile time than newer versions.
And already today, you could possible write a loop instead of recursion.
This is also an exercise for the reader. Post your solution in the comments!
Here a helpful link that shows how to pull a single item from a C++ parameter pack by its index.
Conclusion
Compile-time programming, in general, and special topics like type lists are powerful tools in the C++ toolbox. They are definitely not for everyday use, and not every developer will need to know how to implement them. But they can be very useful in some cases.
The usage of templates is nothing special anymore in day-to-day development, and that is enough for most developers. But for exceptional cases, like moving code around platforms with different integer sizes and ensuring some rules are followed, being able to do some template programming can be very helpful.
In the age of AI, I guess this topic will become increasingly accessible, so it’s good to know about it.