Chapter 5: Miscellaneous Topics on Function Templates in C++
Declaration vs definition, compilation and linking, code organisation, abbreviated templates, and overloading
In this chapter, we will learn about some various aspects of template functions. These are essential but somewhat discrete facts related to the template function. For the completeness of understanding template function, we have clubbed them together in this chapter and will discuss them with appropriate example codes step by step. In this chapter, we will cover the following topics:
Technical Requirements
To follow the examples from this book, you’ll need the following tools:
- Most of the examples in this chapter will work with a standard C++11 compiler.
- Some of the example codes in this chapter may require a C++14 or higher compiler. We will explicitly point it out if there is any compiler dependency to compile the sample code whenever required.
The code files for this chapter can be found on GitHub at: Source Code Chapter 5
Understanding difference between declaration and definition
So far, we have not identified the declaration and definition of the template function separately. In this section, we will see how the declaration and definition of the template function are different and how they can be separated out in the code. Let us start with understanding how to declare a template function in the following section.
Declaring Template function
In C or C++ programming language, we usually declare a function prototype before defining the function. The prototype of a function tells the compiler what the input parameters to the function, their data types, and the return value’s data type are. The template function declaration is similar. In a template function declaration, we tell the compiler what the templated parameters or type parameters, the non-type parameters (if any), and the type of return value of the template function are. To understand this better, let us have a look at the below code snippet:
template<typename T>
T add(const T& x, const T& y);
In this code snippet, we have declared a template function add() with one type parameter T, two function parameters x and y, both of which are a const reference to T type objects. The declaration also tells us that the data type of the return value of add() template function is T. In a template function declaration, there is no function template body. The body of the template function is a part of the definition. In the next section, we will learn how to define a template function.
Defining Template function
Defining a template function means implementing the internal logic of the template function. Like any standard function, the implementation details of a template function go into the template function body. Let us define the template function add() in the following program:
#include <iostream>
template<typename T>
T add(const T& x, const T& y)
{
return x + y;
}
int main(void)
{
std::cout << add(10, 20) << std::endl;
return 0;
}
So in this program we have first defined the template function add() which has a template type parameter T. There are two function parameters to the add() function, x and y. Both x and y are const reference to T type data object. The template function add() performs a very minimal operation — it adds up the input values and returns the result. The return data type of add() is also T. In the main() function of the program we have called add() with two int type data and printed the return value. In short, in declaration we tell the compiler about the input and output parameters of the template function and in definition we implement what the template function is actually supposed to perform. Let us now move on to the next section where we will learn where to declare and where to define the template function.
Declaration before definition
Note that we cannot call a template function before the template function has either been declared or defined in the code. This is also true about non-template functions. Let us first have a look at the following program to understand what we mean:
📎 definition_after_declaration.cpp
#include <iostream>
void foo()
{
std::cout << add(10, 50) << std::endl;
}
template<typename T>
T add(const T& x, const T& y)
{
return x + y;
}
int main(void)
{
foo();
return 0;
}
This program defines a non-template function foo() first and then defines a template function add(). The template function add() has been called inside the foo() function. Because of how this code is written, the foo() function appears in the source code before the definition of the template function add(). Now, if we try to compile the program, we will see the following errors:
$ g++ definition_after_declaration.cpp -o definition_after_declaration -Wall -Werror
definition_after_declaration.cpp: In function 'void foo()':
definition_after_declaration.cpp:4:18: error: 'add' was not declared in this scope
std::cout << add(10, 50) << std::endl;
As you can see, the compiler is complaining that the symbol add() is not declared. The compiler has not come across the symbol add() anywhere before this point in the code and hence doesn’t know what it stands for. To resolve this error, we need to tell the compiler what this symbol is about. The below are the two ways we can resolve this problem:
- Move the definition of the template function before the
foo()function so that when thefoo()function callsadd(), the compiler already has the full definition ofadd(). - Alternatively, we can just declare the template function
add()before we call it infoo(), and define it later in the program. The declaration will tell the compiler that this is a template function, the input parameters of it, and its return type.
There is nothing wrong or right about the two approaches. These are the kind of programming decisions you will often take while working on a large project, and depending on what you want to do, you will design it one or the other way possible. Let us have a look at the next program:
📎 definition_after_declaration_2.cpp
#include <iostream>
template<typename T>
T add(const T& x, const T& y);
void foo()
{
std::cout << add(10, 50) << std::endl;
}
template<typename T>
T add(const T& x, const T& y)
{
return x + y;
}
int main(void)
{
foo();
return 0;
}
As you can see we have added a declaration of the template function add() at the beginning of the file:
template<typename T>
T add(const T& x, const T& y);
And then in the foo() function we have called it. Now if we compile the program there will be no error as shown in the following:
$ g++ definition_after_declaration_2.cpp -o definition_after_declaration -Wall -Wextra -Wpedantic -Werror
$ ./definition_after_declaration
60
In the next section, we will learn more about the compilation and linking of template functions.
Compilation and Linking of template function
In the previous section, we learned that the declaration and the definition of a template function can be separated out. We can just declare a template function without defining it, then call it anywhere in the code and then define it later in the code. While separating the declaration and the definition of the template function may help in organizing the source code better in some cases, it is essential to remember the following point:
This point needs further breaking down of the concept to clearly understand what we can and cannot do. Let us say we have a template function myfoo(), which we declare in a header file A.h and define in a cpp file B.cpp. Now the following is what you cannot do:
- Include the header file
A.hin another cpp fileX.cpp. - Call the template function
myfoo()in the fileX.cpp. - Compile the cpp file
X.cppand cpp fileB.cppseparately and then link them together.
The above will generate a linking error. Remember, this is a difference between template functions and non-template functions. While the above is valid for any non-template functions, it is not valid for template functions. To fully understand the problem here, we need to first understand the compilation and linking process. In the following sub-section, we will learn the steps involved in C++ source code compilation and linking.
Linking of non-template function
Compilation and linking of the source code are complex processes. These depend a lot on the compiler type and the version being used. Also, the organization of the source code introduces more complexities. We will discuss here not to suggest how one organizes the source code or which approach to take for compiling the code, but to emphasize some of the fundamental facts about the compilation and linking process in C++ so that you can make an informed decision. Let us have a look at the following program to understand how linking works in the case of non-template functions. First we will declare a non-template function add() in a header file called function.h:
int add(const int& x, const int& y);
Then we will define the function add() in a cpp file called function.cpp:
int add(const int& x, const int& y)
{
return x + y;
}
Finally, we will write another cpp file called use_function.cpp which can make use of this function add():
#include <iostream>
#include "function.h"
int main()
{
std::cout << add(20, 30) << std::endl;
return 0;
}
In this program, we have included the header file function.h and called the add() function. Now if we compile this program, we will have no error and the program will work just fine:
$ g++ use_function.cpp function.cpp -o use_function -Wall -Werror
$ ./use_function
50
Let us understand this process in detail:
- When we declare a non-template function in a header file and include the header file in a cpp file, the preprocessor copies and pastes the content of the header file to the cpp file. So, in our case, we have included the
function.hheader file in theuse_function.cppfile. After theuse_function.cppfile has gone through the preprocessing step, it would have become a larger file with the content from the header filefunction.has well as its own content. - When
function.cppis compiled the compiler generates the object file for it which has the definition for theadd()function. - Once all the cpp files are compiled — in our case, when both
use_function.cppandfunction.cppare compiled — the linker takes both the object files and links the missing definition ofadd()to the caller of the function. Hence, there is no error.
The following is a pictorial representation of the compilation and linking process for non-template functions.
As you can see in Figure 5.1, the input source files function.cpp and use_function.cpp are being compiled by the compiler. Their respective object files, function.o and use_function.o, have been generated and handed over to the linker. The linker then links the object files together, resolves the symbols and generates the binary executable. With that understanding about compilation and linking of non-template functions in place, we will now learn about the compilation and linking of template functions.
Linking of template function
The compilation and linking of the template function are different from that of the non-template function. The difference stems from the fact that template functions require instantiation by the compiler. To understand the difference better, let us first declare a template function add() in a header file called template.h:
template<typename T>
T add(const T& x, const T& y);
We have declared a template function with one type parameter T. There are two function parameters in the add() template function, x and y, both of which are const reference to T type data. The data type of the return value of template function add() is T. Next, we define the template function add() in a separate cpp file called template.cpp:
template<typename T>
T add(const T& x, const T& y)
{
return x + y;
}
Again, a very minimalistic definition of the template function add(). It takes two function parameters x and y as input and returns the summation of the two values to the caller. Finally, we have written another cpp file called use_template.cpp which includes the header file template.h and in the main() function calls the template function add():
#include <iostream>
#include "template.h"
int main()
{
std::cout << add(10, 20) << std::endl;
return 0;
}
The idea is to compile use_template.cpp and template.cpp together to first create their respective object files by the compiler and then link the object files together by the linker, as we saw in the case of the non-template version of the add() function previously. However, if we try to compile we will see the following linking error:
$ g++ use_template.cpp template.cpp -o use_template -Wall -Werror
/tmp/cczJUP1N.o: In function `main':
use_template.cpp:(.text+0x34): undefined reference to `int add<int>(int const&, int const&)'
collect2: error: ld returned 1 exit status
As you can see, the linker ld has failed to resolve the symbol add(). The reason is no instance of the template function has been generated in any of the cpp files. When we include the template.h header file in the use_template.cpp file, the preprocessor includes the declaration of the add() template function in use_template.cpp. While compiling the cpp file, the compiler comes to know the prototype of the template function add(). Then when the compiler encounters the add(10, 20) call in the main() function, it performs argument deduction — substituting the template parameter T with int — and creates a reference to the add() function with two int data type arguments:
'int add(int const&, int const&)'
However, it cannot instantiate the template function add() as the definition is not available in this file. The assumption here by the compiler is that the definition will be available in some other object file later, and the linker will resolve the reference to add() by linking it to its definition. Now, let us see what is going on with the compilation of the template.cpp file.
The compiler compiles the template.cpp file as a separate compilation unit. Each cpp file, along with all the header files included in it, constitutes a separate translation unit. In the template.cpp file, the add() template function has been defined but not called anywhere. As a result, the compiler did not instantiate it in this file either. So, none of the object files contains the instance of the add() template function for the int data type which the linker is looking for. Hence, the linker complains about the following missing definition:
use_template.cpp:(.text+0x34): undefined reference to `int add<int>(int const&, int const&)'
The following is a pictorial representation of the compilation and linking steps of a template function:
To verify this point we can modify the template.cpp file and introduce a call to the add() function:
template<typename T>
T add(const T& x, const T& y)
{
return x + y;
}
void myfoo(void)
{
add(20, 30);
}
As you can see, we have introduced a function myfoo() in the template_2.cpp file, which is a modified version of the template.cpp file. The myfoo() calls the add() template function with arguments 20 and 30, which are int type data. When the compiler compiles the template_2.cpp, it will encounter the call to the add() template function in myfoo(). The compiler will know that the template function must be instantiated for int type data by argument deduction. So in this case, in the object file of the template_2.cpp file, we now have an instance of the add() template function for int data type which has the following prototype:
int add(const int&, const int&);
So now, if we compile the use_template.cpp and template_2.cpp together, we should not see the earlier error:
$ g++ use_template.cpp template_2.cpp -o use_template -Wall -Wextra -Wpedantic -Werror
$ ./use_template
30
As you can see there is no error this time. This proves our point that separating the declaration and the template function definition doesn’t work the way it works for non-template functions. Because of this reason, we need to have a strategy to organize the template function source code and control its instantiation so that we can use the template functions in the rest of the project. In the next section, we will learn about the possible ways to organize template function code.
Organizing Template function code
One way to organize your template function code will be to define the template function in a header file and then include the header file in the source file which uses the template function. To do that let us first define the add() template function in a header file called sample_header.h:
#ifndef __ADD_H__
#define __ADD_H__
template<typename T>
T add(const T& x, const T& y)
{
return x + y;
}
#endif
Next, we will include the header file in a cpp file called sample_foo_1.cpp. This cpp file will define a function foo_1() which in turn calls the add() template function with arguments 20 and 30:
#include <iostream>
#include "sample_header.h"
void foo_1()
{
std::cout << add(20, 30) << std::endl;
}
Similarly, we will write another cpp file called sample_foo_2.cpp and include the header file sample_header.h in it. In sample_foo_2.cpp, we will write a function foo_2() and call the add() template function with arguments 1 and 2:
#include <iostream>
#include "sample_header.h"
void foo_2()
{
std::cout << add(1, 2) << std::endl;
}
Finally, we write the main cpp file sample_main.cpp, where we define the main() function. The main() function is the entry point in any C++ program. We will also make use of all the previously defined functions, i.e., foo_1() and foo_2(), in this file:
#include <iostream>
#include "sample_header.h"
void foo_1();
void foo_2();
int main()
{
std::cout << add(100, 200) << std::endl;
foo_1();
foo_2();
return 0;
}
Now if we compile and run the program, we will see an output similar to the following:
$ g++ sample_main.cpp sample_foo_1.cpp sample_foo_2.cpp -o sample_main -Wall -Werror
$ ./sample_main
300
50
3
As you can see, the three source files — sample_main.cpp, sample_foo_1.cpp, and sample_foo_2.cpp — are compiled and linked together to generate the binary executable sample_main without any linking issue. A couple of things to notice here:
- We have defined the template function in the header file
sample_header.h. Wherever we need to call the template function we have included this header file. In this approach, as we are including the full definition of the template function by including the header file, the compiler has no problem instantiating the template function when required. - While compiling, we are passing three source cpp files to the compiler as input. The compiler will treat each of these cpp files along with the header files included in it as a separate translation unit.
- At the compilation stage, the symbols in one translation unit are not visible from other translation units.
- If a symbol is referenced but not defined in one translation unit, then in the linking phase, the linker looks for that symbol in all other translation units. If found, the linker resolves the undefined symbol by linking it with its definition.
- Functions like
foo_1()andfoo_2()always have external linkage. External linkage means the scope of this symbol is not limited to this translation unit but across all translation units. Becausefoo_1()andfoo_2()have external linkage, it is sufficient to declare these functions insample_main.cppto call them.
In the next section, we will learn about another important aspect of template functions, which is called abbreviated function templates.
Abbreviated function templates
We can abbreviate the template definition with the help of the keyword auto. The compiler performs the type deduction of auto in a very similar way as it does for templates. Let us have a look at the following example to learn how it works:
#include <iostream>
void foo(auto a)
{
std::cout << a << std::endl;
}
int main()
{
foo(100);
foo('a');
foo(22.2);
return 0;
}
If you compile and run the program, you will see a similar output as the following:
$ g++ function_template_auto.cpp --std=c++17 -o function_template_auto -Wall -Werror -Wextra
$ ./function_template_auto
100
a
22.2
As you can see in the program, we have defined a template function foo(). Instead of declaring a type parameter (for example, T) using the keyword typename, we have declared the template function parameter a using the keyword auto. Then in the main() function, we have passed three different types of input values to the template function foo() — first an integer type value (100), then a character type value ('a'), and finally a double type value (22.2). The compiler has deduced the type of the template function parameter a in each case and has instantiated three different versions of the template function foo(). To verify this, you can use the nm tool (as you learned in Chapter 2, Function Template) as follows:
$ nm -C function_template_auto | grep -i foo
0000000000000a24 W void foo<char>(char)
0000000000000a5b W void foo<double>(double)
00000000000009f0 W void foo<int>(int)
If we remove all the calls to foo() from the main() function and then recompile, we will see no binary code generated for foo() in the generated binary file.
You won’t be able to compile the code if you specify --std=c++11 at the command line:
$ g++ function_template_auto.cpp --std=c++11 -o function_template_auto -Wall -Werror -Wextra
function_template_auto.cpp:3:10: error: use of 'auto' in parameter declaration only available with -std=c++14 or -std=gnu++14
void foo(auto a)
This means the abbreviated templates are not supported in C++11. If you change the compiler type to --std=c++14 or --std=c++17 instead, the code will compile as the use of auto in template function declaration is supported in C++14 and C++17 compilers as a language extension. To check that, you will have to use the compiler flag pedantic:
$ g++ function_template_auto.cpp --std=c++14 -o function_template_auto -Wall -Wextra -Wpedantic
function_template_auto.cpp:3:10: warning: ISO C++ forbids use of 'auto' in parameter declaration [-Wpedantic]
As you can see, we have removed the -Werror flag and added the -Wpedantic flag and the compiler is showing a warning. If you add the -Werror flag as well, the compilation will be forced to fail. In the next section we will learn about overloading template functions.
Overloading template function
Template functions can also be overloaded just like non-template functions. Furthermore, template functions can be overloaded along with their non-template counterparts. When available, the compiler gives preference to the non-template version of the function over the template version. Let us have a look at the following program:
#include <iostream>
#include <string.h>
auto my_size_of(const char *str, size_t& len)
{
printf("%s:%d: size = ", __FILE__, __LINE__);
len = strlen(str);
}
template<typename T>
auto my_size_of(T x)
{
printf("%s:%d: size = ", __FILE__, __LINE__);
return sizeof(x);
}
template<typename T, typename U>
size_t my_size_of(T x, U y)
{
printf("%s:%d: size = ", __FILE__, __LINE__);
return sizeof(x) + sizeof(y);
}
int main()
{
size_t len;
const char * str = "PACKT";
my_size_of(str, len);
std::cout << len << std::endl;
std::cout << my_size_of(10) << std::endl;
std::cout << my_size_of(4.5) << std::endl;
std::cout << my_size_of(10, 2.5) << std::endl;
return 0;
}
In this program, first we have defined a non-template function named my_size_of(), which takes a pointer to a const string object and a reference to a size_t type variable called len. The function my_size_of() calculates the length of the input string object using the library function strlen() and stores the length in the input reference variable len. Because the len variable is passed by reference by the caller, when my_size_of() returns the caller can see the calculated length in the len variable.
Next, we have defined a template function with the same name my_size_of() with a template parameter T and a template function parameter x of type T. In the function body, we calculate the size of the input parameter x using the sizeof() operator and return the calculated size to the caller.
Then we have overloaded the template function my_size_of() with two different template type parameters — T and U — and two function parameters, x of type T and y of type U. In the function body, we calculate the size of x and size of y individually using the sizeof() operator, add their sizes together and return the result to the caller.
In the main() function, we have first called my_size_of() passing a const string "PACKT" and the variable len by reference. Next, we called it passing a value of 10 (type int), then a value of 4.5 (type double), and finally with both 10 and 2.5 as arguments. Now if we compile and run the program, we will see output similar to the following:
$ ./overload_templ_func
overload_templ_func.cpp:5: size = 5
overload_templ_func.cpp:12: size = 4
overload_templ_func.cpp:12: size = 8
overload_templ_func.cpp:19: size = 12
As you can see, the compiler has chosen the right version of the overloaded functions depending on the type and number of arguments. From the output, you can see the compiler has called the non-template version of the my_size_of() function for the string type argument. Then in the next two calls (with the integer and double type arguments respectively), the compiler has chosen the single-parameter template version. In the last call with one int and one double argument, the compiler has chosen the version with two type parameters.
So, in this program we have learned that overloading of template functions is possible and template function overloading works hand in hand with overloading of a non-template function. However, remember the following point about overloading of template functions:
To understand this point let us see the following example:
#include <iostream>
int max(int x, int y)
{
printf("Non-template version called.\n");
return x > y ? 1 : 0;
}
template<typename T>
T max(T x, T y)
{
printf("Template version called.\n");
return x > y ? 1 : 0;
}
int main()
{
int a = 10, b = 20;
auto ret = max(a, b) ? " bigger" : " smaller";
if(ret)
std::cout << "a is bigger " << std::endl;
else
std::cout << "b is bigger" << std::endl;
return 0;
}
In this program, first we have defined a non-template function max() which takes two int type function parameters x and y and returns if x is greater than y. Then we have defined a template function max() with a single type parameter T, which also takes two function parameters x and y and returns if x is bigger than y.
In the main() function we have called max() by passing two int type variables a and b. Now, if you compile and run this program, you will see a similar output like the following:
$ g++ non_template_priority.cpp -o non_template_priority -Wall -Wextra -Wpedantic -Werror
$ ./non_template_priority
Non-template version called.
a is bigger
As you can see from the program’s debug prints, the compiler has chosen the non-template version of the function max(). We have passed two int-type arguments to max() while calling it in the main() function. As there exists a non-template version of max() which takes two int type inputs, the compiler has chosen the non-template version of the function over the template version.
Summary
In this chapter, we learned about miscellaneous topics on function templates. We started our discussion with the difference between function template declaration and definition. We learned that declaration tells the prototype of the function template without implementing the function body. On the contrary, the definition is where the template function’s internal logic is implemented. We have seen how declaring the template function before the definition can help us better organize the template function’s source code.
Then we moved on to the general concept of compilation and linking. We learned about object files, translation units, and how symbols are not transparent to the compiler across translation units. Hence, the compiler depends on the linker for resolving unknown symbols in a translation unit. We learned that it is not possible to separate the declaration and the definition of a template function into two translation units.
Then we learned about how we can better organize the template function code with the help of the header file — we can define the template definition in the header file and include the header file in any source cpp file. Then we learned briefly about abbreviated function templates and overloading of function templates. So far, we have been concentrating on the function templates. In the next chapter, we will learn about class templates.
Questions
- The main problem with function template is that you must declare them before you define them. [True/False]
- It is okay if we define a function template in a cpp file and then call the function template later in the file. [True/False]
- We can declare a function template in a header file, then include the header file in another cpp file and call the function template without having any compilation or linking error. [True/False]
- Translation unit is a cpp file along with all the header files included in the cpp file. [True/False]
- The compiler doesn’t know about symbols in one translation unit while working on another translation unit. [True/False]
- The linker has visibility across all the translation units. [True/False]
- The compiler doesn’t instantiate a template function if the template function hasn’t been called anywhere in the file. [True/False]
- If we just define a template function in a cpp file and then compile the file with a C++ compiler, the compiler will generate binary code for the template function even if the template function hasn’t been called anywhere in the file. [True/False]
- It is perfectly fine to declare a template function at the beginning of a source file, then call the template function in another function in the same file and then finally define the template function in the file. The order doesn’t matter. [True/False]
- In abbreviated template function we use the keyword
autoto indicate that the compiler must deduce the type of the parameter from the passed argument in the template function call during compilation. [True/False] - In template function overloading we can have multiple template functions with the same name but different parameters. [True/False]
- Template function overloading along with non-template function overloading is known as compile time or static polymorphism. [True/False]
Answers
- False
- True
- False
- True
- True
- True
- True
- True
- True
- True
- True
- True
1 Comment