This is the third article in the series of meta programming tutorials. Here we’ll see more basic usage of templates in C++. In this part I’ll talk about non-type template parameters, template of templates, and about passing a function as a template argument.
Next post on series: C++ templates – Beginners most common issue
Non-Type Template Parameters
So far we saw a type template parameter, which is basically means that the function/class specializations are differed by a type/s that passed as a template parameter/s (e.g. int, float, double, std::string, etc...). So what does a non-type template parameter means?
A template that accepts one of the following types:
- lvalue reference
- nullptr_t
- pointer
- enumerator
- integral
Integral
std::array
Probably the most famous usage for a non-type template parameter is for std::array. std::array accepts two template parameters: array type (type parameter) & array size (non-type parameter). Example:
std::array<int, 5> arr;
As you can see, the array size is defined at compile time, and can't be changed during run-time. It might get annoying when you try to create a function that accept the arrays you are working with, when the arrays are deffer in their sizes:
void my_array_func(std::array<int, 5> &arr); // Accepts integer arrays with size 5.
void my_array_func(std::array<int, 6> &arr); // Accepts integer arrays with size 6.
void my_array_func(std::array<int, 7> &arr); // Accepts integer arrays with size 7.
To go around this issue, we should use a non-type template parameter in this function:
template <size_t N>
void my_array_func(std::array<int, N> &arr); // Accepts any integers array.
Or even in a more generic way:
template <typename T, size_t N>
void my_array_func(std::array<T, N> &arr); // Accepts any array.
Compile-Time Mathematics Expressions
Another famous use-case for non-type template parameter are compile-time mathematics expressions calculation.
namespace math {
/* Factorial */
template <size_t N>
struct factorial {
static constexpr auto val = N * factorial<N - 1>::val;
};
template <>
struct factorial<1> { // Partial specialization will be discuss in a future post in this series
static constexpr auto val = 1;
};
/* Pow */
template <size_t Base, size_t N>
struct pow {
static constexpr auto val = Base * pow<Base, N - 1>::val;
};
template <size_t Base>
struct pow<Base, 0> { // Partial specialization will be discuss in a future post in this series
static constexpr auto val = 1;
};
}
int main() {
std::cout << math::factorial<5>::val << std::endl; // Output 120
std::cout << math::pow<2, 10>::val << std::endl; // Output 1024
return EXIT_SUCCESS;
}
The important thing to understand, is that from run-times perspective, the above main would be equivalent to the following one:
int main() {
std::cout << 120 << std::endl;
std::cout << 1024 << std::endl;
return EXIT_SUCCESS;
}
Let's take the factorial for example to explain how does it work:
At first, the compiler instantiate
math::factorial<5>
, but to complete it's val, it should takemath::factorial<5 - 1>
.The compiler instantiate
math::factorial<4>
, but to complete it's val, it should takemath::factorial<4 - 1>
.The compiler instantiate
math::factorial<3>
, but to complete it's val, it should takemath::factorial<3 - 1>
.The compiler instantiate
math::factorial<2>
, but to complete it's val, it should takemath::factorial<2 - 1>
.factorial<1>
already specialized, so the recursion can now collapse.factorial<2>::val
is2 * factorial<1>::val
, which is equal to2
.factorial<3>::val
is3 * factorial<2>::val
, which is equal to6
.factorial<4>::val
is4 * factorial<3>::val
, which is equal to24
.factorial<5>::val
is5 * factorial<4>::val
, which is equal to120
.
Enumerator
The following is a simple example, to print enum values at compile time:
enum languages {
CPP,
C,
CSHARP,
JAVA,
ASSEMBLY
};
template <languages Lang>
constexpr auto stringify() {
if constexpr (Lang == CPP) {
return "C++ language";
} else if constexpr (Lang == C) {
return "C language";
} else if constexpr (Lang == CSHARP) {
return "C# language";
} else if constexpr (Lang == JAVA) {
return "Java language";
} else if constexpr (Lang == ASSEMBLY) {
return "Assembly language";
}
}
int main() {
std::cout << stringify<CPP>(); // C++ language
return EXIT_SUCCESS;
}
Pointers
You can send pointer as a non-type template parameter, as long as it's address is known at compile-time. It's true for static variables:
template<int* T>
int func(int t) {
return (*T)++ + t;
}
int main() {
static int a = 6;
std::cout << func<&a>(6) << std::endl; // Prints 12
std::cout << func<&a>(6) << std::endl; // Prints 13
return EXIT_SUCCESS;
}
Template pointers are usually used to pass another member function as an interface to a class/function:
class general_class {
public:
void func1() {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
void func2() {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
};
template<typename T, void (T::*interface)()> // The interface requires a function which returns void and accepts no params.
class interface_required {
public:
interface_required(T* object) : object(object) {}
void func() {
(object->*interface)();
}
private:
T *object;
};
Now we can call `interface_required` with both of `general_class` function, a single function per instance:
int main() {
general_class gc;
interface_required<general_class, &general_class::func1> ir(&gc);
interface_required<general_class, &general_class::func2> ir2(&gc);
ir.func(); // void general_class::func1()
ir2.func(); // void general_class::func2()
return EXIT_SUCCESS;
}
Another advantage of this usage is that there is less overhead in the call to this object's function. It's like calling a function and not to a member function.
lvalue reference
The same rule for pointers also applied here- the variable address should be known at compile-time.
template<int& T>
int func(int t) {
return T++ + t;
}
int main() {
static int a = 6;
std::cout << func<a>(6) << std::endl; // Prints 12
std::cout << func<a>(6) << std::endl; // Prints 13
return EXIT_SUCCESS;
}
nullptr_t
we we'll discuss this in a future post, it's part of more advanced meta-programming topic.
Template of Templates
An important thing to mention, is that this ability usually doesn't really necessary to use, however it might make to code easier to write and to understand, so don't avoid a friendship with it.
Sometimes, especially when writing a library, it's important to make the code as generic as possible, and we don't want to limit library users to use a specific container type (e.g. std::vector, std::set, std::list, etc..). There are many ways to archive this generic target, and the following example is one of them:
#include <numeric>
#include <vector>
template <typename Cont>
auto sum(Cont &container) {
typedef typename Cont::value_type T;
return std::accumulate(std::begin(container), std::end(container), T(0));
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6};
int res = sum(vec); // Use compiler deduction
// int res = sum<std::vector<int>>(vec);
return EXIT_SUCCESS;
}
It worked, and I'll expend the talk about `auto` in a future post in this series (for now it's a type that deduced at compile time by it's first usage). But there is a line that made this simple code to a very unpleasant:
`typedef typename Cont::value_type T;`
It seems that there have to be a better way to get this type. Let's use template of templates:
#include <numeric>
#include <vector>
template <typename T, template<typename, typename = std::allocator<T>> class Cont> // Since C++17 the class can be also a typename
T sum(Cont<T> &container) {
return std::accumulate(std::begin(container), std::end(container), T(0));
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6};
int res = sum(vec); // Use compiler deduction
// int res = sum<std::vector, int>(vec);
return EXIT_SUCCESS;
}
This line disappeared and we still got the same result. So all we have to do now is to inspect the template line:
template <typename T, template<typename, typename = std::allocator<T>> class Cont>
typename T
- will represent the type that the container will contain.template<typename, typename = std::allocator<T>>
- The first type will accept our T
, and the second is required for the standard library (std) containers, and by default (which we'll use) is the standard allocator of T (as we do most of the times we use the standard containers.class Cont
- It's the name of the template param type. Until C++17, we had to use the class
keyword for these cases of templates-templates, however since C++17 we can still use typename
as following:
template <typename T, template<typename, typename = std::allocator<T>> typename Cont>
Most of the necessary usages of templates-templates is on structs/classes with variadic template parameters (don't panic!), but here is a rare case when you have to use it on function:
template <typename OutContType, template <typename, typename> class C, typename InContType>
auto cast_all(const C<InContType, std::allocator<InContType>> &c) {
C<OutContType, std::allocator<OutContType>> result(c.begin(), c.end());
return result;
}
int main() {
std::vector<double> double_vec = {1.5, 2.3, 3.6};
std::vector<int> int_vec;
int_vec = cast_all<int>(double_vec);
// cast_all<int, std::vector, double>(double_vec);
return EXIT_SUCCESS;
}
A simple casting from one container type to the same container of another type, and you have to pass the container as template of template, or else you won't be able to use the container for another type.
A simplification of this function I'll show in a future post about variadic template parameters.
Passing Function as a Template Argument
This section is only a spoiler for a future post about variadic template parameters, just to prepare you to this subject.
Passing function pointer to another function is something that came all the way from the procedural C language:
void qsort (void* base,
size_t num,
size_t size,
int (*comparator)(const void*,const void*));
In C++, since C++11 (or using boost::function for earlier C++ versions), there is std::function that make the syntax more readable:
std::function<int(void*, void*)> &&comparator
The main issue with this call is the performances. There is an overhead when calling a function with a std::function parameter. To avoid this overhead, we can pass a function as a template parameter:
template<typename FuncT>
int max(std::vector<int> vec, FuncT &&comparator) {
int res = vec[0];
for (auto &elem : vec)
if (comparator(res, elem) < 0)
res = elem;
return res;
}
Now that we have a generic fucntion parameter, let's make out function generic too:
template<typename Cont, typename FuncT>
auto max(Cont container, FuncT &&comparator) {
auto res = *std::begin(container);
for (auto &elem : container)
if (comparator(res, elem) < 0)
res = elem;
return res;
}
int main() {
std::vector<int> int_vec = {1, 30, 20, 10};
std::list<double> double_list = {5.2, 2.9, 6.3, 4.8};
// Since C++20 template params are allowed in lambda expressions. Since C++14 we can also use `auto`, but here `auto` is too generic for this function.
auto comp = [] <typename T> (T n1, T n2) -> auto {
return n1 - n2;
};
std::cout << max(int_vec, comp) << std::endl;
std::cout << max(double_list, comp) << std::endl;
return EXIT_SUCCESS;
}
Conclusion
I hope you learned something new from the second part of basic generic usage of templates. From now on we are going deeper into the meta-programming subjects, and we'll see more abilities and more usages of meta-programming in modern C++.
Have something unclear? A request for a future post? Want to share new things you learned? Feel free to share!
This post originally published on my personal blog: C++ Senioreas.
Top comments (0)