C++ metaprogramming and templates are powerful features that allow you to write code that operates on types and constants at compile time. This capability can lead to more flexible, efficient, and reusable code.
Templates are a feature in C++ that allow functions and classes to operate with generic types. This means you can write a function or class that works with any data type. Templates can be of two types: function templates and class templates. A function template defines a blueprint for a function that can work with any data type, while a class template allows a class to operate with generic types.
Metaprogramming involves writing programs that manipulate other programs (or themselves) as their data. In C++, this is often done using templates, leading to compile-time computation. Template metaprogramming (TMP) can be used to perform calculations, generate code, and enforce constraints during compilation.
We do not dive into the details of C++ metaprogramming and templates and assume the readers are already experienced with C++ templates. Now, let us say we would like to implement a micro DSL for templates parameters - when specific conditions are met, the DSL will generate the corresponding class. For example, when the size of a container is available during compile time and is larger than 1000, the template class generates a std::array
as the concrete implementation for the data container; otherwise, the template class generates a self-implemented array
.
Firstly, we would like to decide whether a list of type parameters contains a certain type, for example, if char
is included in the type parameters list int, float, double, float
. As the list of type parameters to be checked can be of various size, there we need to treat it as a variadic type parameter
. Variadic templates make it possible to write templates that can take an arbitrary number of template arguments. These can be used in both function templates and class templates.
1template<typename T, typename... Ts>
2struct contains: std::disjunction<std::is_same<T, Ts>...> {};
Here, the type parameter T
is checked against the variadic template parameters Ts
one by on with the type_trait std::is_same
and then apply a logic AND
operation on each of the comparision with std::disjunction
.
With contains
available, we continue to implement a selects
.
1template<typename... Args >
2using selects = typename std::disjunction<Args...>::type;
Implementing a selects
primitive is straightforward with as we only need to retrieve its inner type.
The tricky part is the if clause, how do we decide wheter the condition should be satisfied or not?
1template<bool V, typename T>
2struct when {
3 static constexpr bool value = V;
4 using type = T;
5};
Here, we have a non-type paramter V
of boolean
type, which should be the outpt from our contains
. The type parameter T
here should be the type if the condition is met, which mimics the usage of the block of code to be executed if the condition in the if statement
is true. A quick question for some readers - why the boolean value V
should be declared as static constexpr
?
Then, we are pretty much done! To use our micro DSL, we can write a statement as,
1selects<when<condition1, result1>, when<condition2, result2>>
Great! Do you want to know more about how to extend our micro DSL or how to apply this micro DSL in your C++ project? Then get in touch :)
Comments