In the previous chapter, we learned all about function templates. While learning about function templates, we came across the terminology template parameter. In this chapter, we will learn about template parameters in more detail. We will learn about different types of them and their usages.

You can access all the sample programs used in this book in this Git repository: Source Code Chapter 3.

Introduction to Template Parameters

Before going into template parameters in more detail, let us first have a look at function parameters. Function parameter is a mechanism by which we can pass values to a function. When we define a regular (non-template) function, we tell the compiler what the parameters for the function are and their respective data types. For example, consider the following piece of code:

int foo(int x, int y)
{
    return x + y;
}

We have defined a function foo() here, with two function parameters, x, and y, type int. We can call the function foo() passing int type data. The types of the parameters are predefined. There is no way we can pass the data type of the parameters in a regular function while calling it. On the contrary, a template function enables us to pass the data type of the parameters x and y. A special type of parameter, which is called the template parameter, is used to pass the data types of the function parameters. For example, in the following template function definition:

template<typename T>
bool is_equal(T x, T y)
{
    return x == y;
}

T is the template parameter, and it allows us to pass the type of the template function parameters x and y to be passed. There are different types of template parameters. Primarily we can divide the template parameters into the following categories:

Type parameters

So far, the template parameters we have seen are all type parameters. A type parameter represents a generic data type substituted by an actual data type during compilation by the compiler. The actual data type is deduced from the arguments passed in the template function call.

Non-type parameters

The non-type template parameter, on the contrary, represents a compile-time determinable constant.

While discussing type and non-type template parameters, do not forget about the regular non-template function parameters. They can work in tandem with the template parameters in a template function.  In the next section we will learn bout type parameters and then in subsequent section about non-type parameters.

Working with type parameters

We will start our discussion with the type parameters in this section. In the following example, we will see a template function that takes a template (type) parameter and a non-template parameter. Let us have a look at the program as the following:

dynamic_allocator.cpp

#include <iostream>
#include <string.h>
template<typename T>
T* dynamic_allocator(T val, const size_t N)
{
    T *mem = new T[N];
    for (size_t index = 0; index < N; ++index)
        mem[index] = val;
    return mem;
}
int main()
{
    size_t len = 0;
    len = 10;
    auto iptr = dynamic_allocator(5, len);
    for (size_t index = 0; index < len; ++index)
        std::cout << "main:: " << iptr[index] << std::endl;
    len = 5;
    auto dptr = dynamic_allocator(20.7, len);
    for (size_t index = 0; index < len; ++index)
        std::cout << "main:: " << dptr[index] << std::endl;
    len = 3;
    auto cptr = dynamic_allocator('C', len); 
    for (size_t index = 0; index < len; ++index)
        std::cout << "main:: " << cptr[index] << std::endl;
    delete [] iptr;
    delete [] dptr;
    delete [] cptr;
    return 0;
}

In the previous program, we have implemented a dynamic memory allocator. The function template dynamic_allocator() takes template type parameter val and a regular non-template parameter N. In the main() function, we are calling the dynamic_allocator() with three different sets of arguments:

Case 1

    auto iptr = dynamic_allocator(5, len);

In the first case, we are passing a value 5 and a non-template argument len. The compiler looks at the function template call, deduces T’s type to be int (as 5 is an int value), and instantiates the dynamic_allocator() with type int. In the function body, we are allocating memory for len number of int type objects and returning the memory address of the newly allocated memory.

Case 2

    auto dptr = dynamic_allocator(20.7, len);

In the second case, we call the dynamic_allocator() with a value of 20.7 and non-template argument len. The compiler looks at the value 20.7 and deduces its type to be double (20.7 is a double value), replaces T with type double, and instantiates dynamic_allocator() with type double. In the function template body, we are allocating memory for len number of double type variables and returning the pointer to the newly allocated memory.

Case 3

    auto cptr = dynamic_allocator('C', len);

In the third case, we pass the first argument as ‘C’ and the second argument as a non-template argument len. As before, the compiler interprets the type of ‘C’ as char, instantiates the function template with type char. In the function template body, a memory area for len number of char type variables is allocated, and the pointer to the newly allocated memory is returned.

As you can see, the template type parameter (T in this case) is deduced by the compiler when the template function is called with actual arguments. The non-template argument N is just like any ordinary function parameter for which the type is already specified in the function template definition. It can be a built-in or user-defined data type. Evidently, the non-template parameter(s) can cohabitate with the template (type) parameter(s) in a template function.

In the next section, we will learn about non-type template parameters.

Working with non-type parameters

Non-type template parameters are different than the type parameters. While the type parameters are substituted with the compiler deduced actual data type, constant values substitute the non-type template parameters at compile time. The value must be determinable at the compilation phase or linking phase. The following are the commonly used non-type parameters:

  • Enums
  • int
  • Pointer to class objects
  • Pointer to functions
  • std::nullptr_t

In the following section, we will look at the non-type template parameter of the enum type.

Enums as non-type template parameter

While declaring the non-type template parameter you may use an enum data type. The following example will make this clear. Let us have a look at the following example code:

#include <iostream>
enum myenum {
    ONE = 1,
    TWO = 2,
    THREE =3
};
template <myenum val> //pointer to object
void f()
{
   std::cout << val << std::endl;
}
int main()
{
    f<ONE>();
    return 0;
}

As you can see, we have defined an enum called myenum and then defined a template function with a non-type template parameter named val. While calling the template function, we can only pass the defined enums (i.e., one, two, and THREE). In the following section we are going to see usage of integer type as non-type template parameter.

Integer as non-type template parameter

Integers can be declared as a non-type template parameter. To illustrate that, let us reuse our dynamic allocator code. Let us modify the dynamic allocator program and introduce a non-type parameter to pass the size of the allocation as shown in the following listing:

dynamic_allocator_non_type.cpp

#include <iostream>
#include <string.h>
template<size_t N, typename T>
T* dynamic_allocator(T val)
{
    std::cout << "Value of N = " << N << std::endl;
    T *mem = new T[N];
    for (size_t index = 0; index < N; ++index)
        mem[index] = val;
    return mem;
}
int main()
{
    const size_t len = 10;
    auto iptr = dynamic_allocator<len>(5);
    for (size_t index = 0; index < len; ++index)
        std::cout << iptr[index] << " ";
    std::cout << "\n";

    auto dptr = dynamic_allocator<5>(20.7);
    for (size_t index = 0; index < 5; ++index)
        std::cout << dptr[index] << " ";
    std::cout << "\n";

    const size_t len3 = 3;
    auto cptr = dynamic_allocator<len3>('C');
    for (size_t index = 0; index < len3; ++index)
        std::cout << cptr[index] << " ";
    std::cout << "\n";
    delete [] iptr;
    delete [] dptr;
    delete [] cptr;
    return 0;
}

Notice in the earlier implementation of the dynamic allocator, N was a regular function parameter. But in this implementation, N has been declared as a non-type template parameter like the following:

template<size_t N, typename T>

The type of N is size_t. You can further qualify N’s type as const as well. Now, let us see how to pass the non-type parameter to the template function while calling it. We are passing the non-type parameter N as the following while calling it from the main() function:

    const size_t len = 10;
    auto iptr = dynamic_allocator<len>(5);

As you can see, we have declared a const data len of type size_t and initialized it with a value of 10. Then we passed len as a non-type parameter to the template function dynamic_allocator within angle brackets. Remember len must be a constant and hence you cannot drop the const qualifier here. Without the const qualifier, len will turn into a size_t type variable whose value can change at runtime. Variables are not allowed as a non-type parameter.

Note: You cannot pass a variable as non-type argument. It must be a constant.

To prove this point try dropping the const qualifier and then pass it to dynamic_allocator as the following:

size_t len = 10;
auto iptr = dynamic_allocator<len>(5);

Now if you try to compile the code the compilation will fail with the following error message:

$ g++ dynamic_allocator_non_type.cpp -o dynamic_allocator_non_type -Wall  -Wextra -Wpedantic -Werror
dynamic_allocator_non_type.cpp: In function 'int main()':
dynamic_allocator_non_type.cpp:15:35: error: the value of 'len' is not usable in a constant expression
     auto iptr = dynamic_allocator<len>(5);
dynamic_allocator_non_type.cpp:14:12: note: 'size_t len' is not const
     size_t len = 10;

As expected, the compiler is complaining that len is not a constant. One more thing to notice in this example is the order of the template parameters in the declaration. We have declared the non-type parameter N first in the list and then the type parameter T. We must maintain the same order while calling the template function. From the main() function, we called the template function like the following:

dynamic_allocator<len>(5) 

We are passing the constant len first within angle brackets, and then the value 5 within braces. Now, let us declare T first and then N in the template parameter list as the following:

template<typename T, size_t N>

With this declaration, then the below calling mechanism will not work:

    	auto cptr = dynamic_allocator<len>(5);

Here, the non-type and type argument order in the template function call does not match the new template parameter declaration order. Hence this will fail in the compilation. To get this working, we must explicitly tell the compiler the type that must substitute T and the constant to be used to replace N, both within angle brackets like the following:

auto iptr = dynamic_allocator<int, len>(5);
auto dptr = dynamic_allocator<double, len2>(20.7);
auto cptr = dynamic_allocator<char, len3>('C');

In the next section we will see how we can pass a pointer type as a non-type template parameter.

Pointer as non-type template parameter 

You cannot pass regular pointers as a non-type parameter to the template function. Pointers contain addresses of other variables. We cannot pass variables as non-type parameters to template functions. Let us have a look at the following program:

non_type_ptr.cpp

#include <iostream>
#include <string.h>
#include <malloc.h>
char *ptr;
template<char * str>
void print_str()
{
    std::cout << str << std::endl;
}
int main()
{
    ptr = (char *) malloc(32 * sizeof(char));
    strcpy(ptr, "Hello World");
    print_str<ptr>();
    return 0;
}

In this example, we have defined a template function print_str with a non-template parameter str, a char-type pointer. In the main() function, we have allocated memory calling malloc() library function and assigned the address to the allocated memory to a char type pointer, ptr. If you try to compile this program, you will see the following compilation error:

$ g++ non_type_ptr.cpp -o non_type_ptr -Wall  -Wextra -Wpedantic -Werror
non_type_ptr.cpp:6:6: note:   template argument deduction/substitution failed:
non_type_ptr.cpp:14:20: error: 'ptr' is not a valid template argument because 'ptr' is a variable, not the address of a variable
     print_str<ptr>();

The error message is pretty much self-explanatory. ptr is a pointer-type variable and hence not allowed as a non-type argument to a template function. The compiler is complaining about that.

However, you can pass a char type array as a non-type parameter. Let us have a look at the following code snippet:

non_type_ptr_object.cpp

#include <iostream>
#include <string.h>
char name[256];
template<char * str>
void print_str()
{
    std::cout << str << std::endl;
}
int main()
{
    strcpy(name, "Hello World");
    print_str<name>();
    return 0;
}

Here, we have defined a template function print_str with a non-type template parameter str, a char type pointer. We declared a char type array called name. In the main() function, we have copied a string “Hello World” into the name array and then passed it in the template function call as the following:

    print_str<name>();

Here name is the non-type template parameter which is an array. In the next section we will see how to pass a pointer to a class object as non-type parameter to template functions.

Pointer to objects as non-type template parameter

Pointer to a class object is the addresses of the object of any class. Let us have a look at the following example to understand this:

non_type_ptr_to_class_object.cpp

#include <iostream>
class A {
public:
    void welcome() {std::cout << "Hello World" << "\n";}
};
class A obj_A;

template<class A * str>
void print_str()
{
    str->welcome();
}
int main()
{
    print_str<&obj_A>();

    return 0;
}

As you can see, in this program, we have defined a class called A which has a single member function welcome() which prints “Hello World.” We have also defined a template function print_str with a non-type template parameter str, a pointer to class A. We have created an object obj_A of class A in the global space. Then from the main() function, we have passed the address of obj_A to the template function print_str. Now, if we compile and the program, we will see output like the following:

$ g++ non_type_ptr_to_class_object.cpp -o non_type_ptr_to_class_object -Wall  -Wextra -Wpedantic -Werror
VBR09@mercury:~/CPP-Templates-Up-and-Running/Chapter_3$ ./non_type_ptr_to_class_object
Hello World

In the next section we will see how to pass reference to class object as non-type template parameter.

Reference to class object as non-type template parameter

We can pass references to class objects as a non-type parameter to template functions. Let us see this in the following example code:

non_type_ref_object.cpp

#include <iostream>
#include <string>
std::string mystr;
template<std::string& str>
auto get_str_len()
{
    auto len = str.length();
    return len;
}
int main()
{
    mystr = "Hello World";
    std::cout << "Length of mystr = " << get_str_len<mystr>() << std::endl;
    return 0;
}

In this example, we have defined a template function get_str_len with a non-type template parameter named str, which is a reference to a std::string type object. The std::string is a standard C++ string class that represents a string i.e., a sequence of characters. To use it in your code, you must include the string class. We have also declared a std::string type object mystr in the global scope. In the main() function, we have then assigned the string “Hello World” to the object mystr and passed it as a non-type parameter to get_str_len. If we compile and run the program, we will see an output like the following:

$ g++ non_type_ref_object.cpp -o non_type_ref_object -Wall  -Wextra -Wpedantic -Werror
$ ./non_type_ref_object
Length of mystr = 11.

Note, we cannot declare mystr within the main() function or any other function. If we declare mystr within a function, it will have no linkage. If we want to pass it as non-type template parameter, it must have an external linkage.

Pointer to function as non-type template parameter

You can also pass a pointer to a function as a non-type parameter to a template function as well. Let us have a look at the following code to understand this better:

non_type_func_ptr.cpp

#include <iostream>
template<void (*fnc)()>
void welcome_message()
{
    fnc();
}
void dymmy_welcome()
{
    std::cout << "Hello World" << std::endl;
}
int main()
{
    welcome_message<dymmy_welcome>();
    return 0;
}

As you can see, in this program, we have defined a template function welcome_message which has a non-type template parameter fnc which is a pointer to a function. In addition, we have defined a function dummy_welcome() which prints “Hello World” on the screen. In the main() function, we have passed the dummy_welcome() function name as a non-type parameter in the call to welcome_message within angle brackets. Notice that the prototype of the dummy_message() function matches with the declaration of the function pointer fnc in the welcome_message declaration.

In the next section, we will see how multiple template parameters can be passed to template functions.

Working with multiple type-parameters

So far, we have seen a single type parameter in function templates. You can have more than one type parameter in the function template. Let us have a look at the following program:

address_book.cpp

#include <iostream>
#include <map>
#include <utility>
#include <sstream>
std::multimap<std::string, std::string> addressbook;
template<typename T, typename U>
void add_data_to_store(T key, U data)
{
    std::ostringstream s;
    s << data;
    addressbook.insert(std::make_pair(std::string(key), s.str()));
}
void display_store()
{
    for (auto m : addressbook)
        std::cout << m.first << " " << m.second << std::endl;
}
int main()
{
    add_data_to_store("John", 25);
    add_data_to_store("John", 1000.00);
    add_data_to_store("John", "74 Grisham Road");
    display_store();
    return 0;
}

In this program, we are implementing an address book. To do that, we have defined a function template add_data_to_store() which takes two type parameters, T and U. To implement the address book, we need to choose a data structure that can not only hold the data but the data can be retrieve easily whenever required. For this purpose, we will use an STL container called multimap for holding our address book entries. In a multimap, we have key and value pair for each entry. The key uniquely identifies an entry to the address book, and the value represents the actual value of the entry. However, there can be multiple entries for the same key, hence called multimap.

The parameter T represents the key to the data store, and U represents the data. In this particular case, the type of the key is string. However, the data types vary from int (age) to double (salary) to string or char * (address). From the main() function, we have passed three different key and value pairs. For a particular entry, the key remains the same, i.e., the person’s name (“John” in our case).

However, the data may change, as in our case the data is varying from int (age) to double (salary) to char * (address). Multimap accepts multiple entries with the same key. In our case, the key “John” remains the same in all three cases, but the data is different in each entry. The template function converts the input data into a std::string type using stringstream and then inserts it into the data store. The display_store() function prints the store’s content. The output of this program will look as shown:

$ ./address_book
John 25
John 1000
John 74 Grisham Road

In this example we have seen how to define function template with multiple type parameters. In the next section, we are going to learn how the compiler deduces the type for different arguments passed to the template function call.

Summary

In this chapter, we learned about the template parameter. We looked into different categories of template parameters – the type and the non-type parameters. Then we discussed in detail what type parameters are and how they are used in the template functions. Next, we examined what the non-type parameters are. We went over each type of non-type template parameter and its usage. We also learned how to define and use template functions with more than one type of parameter. It is essential to understand template parameters as they are the crux of template programming. In the next chapter, we will learn about another aspect of the template function: the return type.

Questions

  1. What does the type parameter represent in a template function declaration/definition?
  2. Can type parameter and non-type parameter coexist in a template function definition?
  3. How many type parameters can there be in a template function declaration/definition?
  4. Can you use sizeof() operator on a array type argument if passed to a template function by value?
  5. Which one is a type parameter and which ones are the template function parameter in the following piece of code?

Template<typename T>

T add(T x, T y)

{

    Return x + y);

}

6. How many type parameters are there in example code shown in the previous question?

7. When does the array argument decays into a pointer, in pass by value or pass by reference?

8. Is the const qualifier in the template function definition guarantees the preservation of const-ness of the argument(s) passed?

9. User defined data types cannot be passed to a template function. (True/False)

10. How can we pass an array to a template function so that the array doesn’t decays into a pointer?

Answers

  1. Type parameters represent a generic type which is substituted with the real type by the compiler when the template parameter is called in the program later on.
  2. Yes.
  3. There is no restriction on the number of type parameters, technically as many as required. However, most of the time you will see two type parameters are sufficient for practical purposes.
  4. No. As the array decays into a pointer when the array is passed by value to a template function, the sizeof() operator will only operate on the address of the first element of the array and not on the whole array.
  5. Type parameter à T and template function parameter à x and y.
  6. One type parameter which is T.
  7. In pass by value.
  8. No. It depends on how the argument has been passed to the template function (by value or by reference) and also what kind of argument has been passed.
  9. False.
  10. Pass the array by reference.

Leave a Reply