DEV Community

Coral Kashri
Coral Kashri

Posted on • Originally published at cppsenioreas.wordpress.com on

C++ Basic templates usage – Part 2

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;
Enter fullscreen mode Exit fullscreen mode

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&lt;int, 5> &amp;arr); // Accepts integer arrays with size 5.
void my_array_func(std::array&lt;int, 6> &amp;arr); // Accepts integer arrays with size 6.
void my_array_func(std::array&lt;int, 7> &amp;arr); // Accepts integer arrays with size 7.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 take math::factorial<5 - 1>.

  • The compiler instantiate math::factorial<4>, but to complete it's val, it should take math::factorial<4 - 1>.

  • The compiler instantiate math::factorial<3>, but to complete it's val, it should take math::factorial<3 - 1>.

  • The compiler instantiate math::factorial<2>, but to complete it's val, it should take math::factorial<2 - 1>.

  • factorial<1> already specialized, so the recursion can now collapse.

  • factorial<2>::val is 2 * factorial<1>::val, which is equal to 2.

  • factorial<3>::val is 3 * factorial<2>::val, which is equal to 6.

  • factorial<4>::val is 4 * factorial<3>::val, which is equal to 24.

  • factorial<5>::val is 5 * factorial<4>::val, which is equal to 120.

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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*));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)