Cpp chronicle #2

C++ template basics - 1

Template is a great C++ language feature, with which we can make classes and functions operate on generic types without actually writing code for all possible valid types.

For example std::vector container in C++11 is a template class. We can use it for with different value types, with the following syntax.

std::vector<int> v1;
std::vector<char> v2;

Here, v1 can hold values of type int and v2 can hold values of type char.

In this blog we will be discussing basic template usage in C++. So, let’s get started.


All the examples in this blog will consider function templates, i.e. functions with template parameters. However most of the ideas can be applied to a class template also. Let`s look at the following example.

[Example 1]


#include <iostream>
#include "PrintHelper.hpp" // is a helper utility header for printing
using namespace std;


template<typename T>
void func(T x) {

    PrinF;       // prints signature of this function

    // do something with x

}


int main() {

    func(1);        // auto deduce
    
    BreakLine;      // prints a newline
    
    func("string"); // auto deduce

}

Let`s view the output of the above program.

8:void func(T) [with T = int]


8:void func(T) [with T = const char*]

Not to confuse, 8 is just the line number here, printed using PrintF utility function.

In this example, func is a function templated with type paramter T, and func takes an argument of type T.

From the main function we can pass any kind of values to the function func and the type of T will be deduced.

Lets try to print the type of x using our print utility functions. (ignore the type_name utility for now. It just prints the required type )

template<typename T>
void func(T x) { 
    PrinF;

    // prints the type of x
    std::cout <<"Type(x) : "<< type_name<decltype(x)>() << std::endl;

    // prints the type of T
    std::cout <<"Type(T) : "<< type_name<T>() << std::endl;

    // do something with x
}

Here is the output:

8:void func(T) [with T = int]
Type(x) : int
Type(T) : int


8:void func(T) [with T = const char*]
Type(x) : char const*
Type(T) : char const*

It seems that type of T and type of x is same, which is very intuitive also in this particular example. But that might not be always true as we will see below.

Whenever we write func(some_value) compiler tries to find Or resolve the type of T as well as type of x. This type resolving task is also called as template type deduction. With these rules of template type deduction the deduction happens in two folds.

step 1. Deduction of T
step 2. Deduction of type of argument (x)

The iteresting thing is that, step 1 depends on how we declare the function arguments.


template<typename T>
void func(T x) {

In our current example, we declare the arguments as T x. With type deduction rules, the type of x and T will be same in this case.

Lets try to change the argument declaration to const T& x (why const ?? it has to do with lvalue or rvalue binding. we will not discuss that now)


#include <iostream>
#include "PrintHelper.hpp"
using namespace std;


template<typename T>
void func(const T& x) {
    PrinF;
    std::cout <<"Type(x) : "<< type_name<decltype(x)>() << std::endl;
    std::cout <<"Type(T) : "<< type_name<T>() << std::endl;
}


int main() {

    func(1);        // auto deduce
    
    BreakLine;
    
    func("string"); // auto deduce

}

Output:

8:void func(const T&) [with T = int]
Type(x) : int const&
Type(T) : int


8:void func(const T&) [with T = char [7]]
Type(x) : char [7] const&
Type(T) : char [7]

As we can see that, in the instantiation of func("string") : T becomes char [7] (array of 7 characters) and type of x becomes char [7] const& which is reference to an array of const char of length 7

The two fold step becomes evident with rvlaue references with argument declaration as T&& x. We will dicusss this in the next blog.

OK. Enought of type deduction. But type deduction rules are very important. To know more about the rules of C++ template type deduction, there is a whole talk on it by Scott Meyers. Here is the link from cppcon 2014.

So, in a non template world, we would have written two different function to handle int and strings. Therefore the basic question is how a single definition of func handling different types ?

The answer is : "Compiler generates required functions for us". So in the above example, we have two instances of calling func(....), one with int and the other one with const char*. Compiler sees this and generates two function definitions for us. We can check this in gdb.

// compile the code
g++ -g -ggdb -std=c++11 p01.cpp -fsanitize=address -fsanitize=leak
// run gdb
gdb ./a.out
>>> info functions func
All functions matching regular expression "func":

File p01.cpp:
void func<char [7]>(char const (&) [7]);
void func<int>(int const&);

Moving on, there can be three different types of template parameters:

template < parameter-list > 
declaration

Each parameter in parameter-list may be:

1. a non-type template parameter
2. a type template parameter
3. a template template parameter.

We have only discusseed 2. a type template parameter in the above examples.

Lets know about 1. a non-type template parameter

A non-type template parameter is an expression whose value can be determined at compiler time such as constant expression and address of function etc. More on this.

Here is an example: [Example 2]

#include <iostream>
#include "PrintHelper.hpp"
using namespace std;


template<int N>
void func(int indx) {

    // determine the size of the buffer using non-type template parameter
    static char x[N];
    
    // just doing some opration with the array
    if(indx < N && indx >= 0) {
        x[indx] = indx*indx;
    }

    // print the array
    for(int i = 0;i<N;i++) {
        printf("x[%d] = 0x%x ",i,x[i]);
    }
    printf("\n");
}


int main() {

    func<6>(0);
    func<6>(1);
    func<6>(2);
    func<6>(3);

}

Before viewing the output, lets check the template intantiation syntax in the above example.

func<6>(args ...) this is one more way specifying the template parameter.

If we consider the earlier example, we could have instantiate func template function as follows:

func<int>(1);
func<string>(std::string("abc"));
func<some_type>(value_of_that_type)

Back to the example 2, func<6>(args ...) is going to fix the template parameter N = 6 and the compiler is going to create a character array of size 6.

Here is the output:

x[0] = 0x0 x[1] = 0x0 x[2] = 0x0 x[3] = 0x0 x[4] = 0x0 x[5] = 0x0 
x[0] = 0x0 x[1] = 0x1 x[2] = 0x0 x[3] = 0x0 x[4] = 0x0 x[5] = 0x0 
x[0] = 0x0 x[1] = 0x1 x[2] = 0x4 x[3] = 0x0 x[4] = 0x0 x[5] = 0x0 
x[0] = 0x0 x[1] = 0x1 x[2] = 0x4 x[3] = 0x9 x[4] = 0x0 x[5] = 0x0

GDB output also using info functions command:

>>> info functions func
All functions matching regular expression "func":

File p02.cpp:
void func<6>(int);

Although this example of non type template argument does not give much usefulness, but we will see better examples in the upcoming blog.

What next ?

So, in the next blog, we will discuss on template template parameter (the 3rd type of template parameters) and template specialization. We will also discuss templated class or structs. Till then why not check this awesome talk on Runtime Polymorphism by Sean Parent.


See also