CS202 Programming
Systems
Lecture Notes #10
Template Functions
and
Template Classes
Lecture
and Reference
Function Template: A parameterization of type dependencies that allows the compiler to automatically generate as many overloaded functions as needed. A general purpose function template is called the primary function template.
Template Function: A specific instance of a function that is instantiated (i.e. generated) from a function template.
Specialized Template Function: Specialized versions of a template function that work with cases that don't fit the normal pattern.
Function templates allow us to isolate type
dependencies within a function so that the compiler can create a family of
related functions as the need arises. Using function templates, we shift the
burden of creating new functions that differ only in the data types to the
compiler. This is primarily useful when functions perform the same tasks on
different types of data. A function
template is the definition of a parameterized family of functions. A
general purpose function template is called the primary function template. A template
function is a particular instance of this family.
Without templates, we might find ourselves rewriting the same function over and over again to work with different types of data. For example, we might implement the same quicksort routine three different times to sort integers, reals, or characters. With function templates, we can write the quicksort function just once and have compiler apply it to each type of data used.
Specialized template functions allow us to create specialized versions of a template function to work with cases that don't fit the normal pattern. For example, the code inside a quicksort routine would need to be different when sorting integers, reals, or characters versus character strings. With specialized template functions, we can support such differences, promoting reuse as well as the ability to customize for particular data types. Specialization template functions are commonly written to support pointers and other compound data types that behave differently than the fundamental data types.
The following sections introduce us to the concept of function templates by stepping us through the process of defining and declaring both function templates and specialized template functions. Then we discuss how function calls are resolved and how we can explicitly specify type dependencies in our function calls. We conclude with some words of advice and a complete example of a function template.
10.1 Type Dependencies
Type Dependencies: Generalized data types that can be replaced by specific data types when a function is invoked.
In many cases, we write code where the only difference between one function and another is the type of data being manipulated. For example, a function to copy one array to another is basically the same regardless of whether the array being copied is an array of integers, an array of floats, or an array of characters. By implementing a single function template, we can write one function to perform the copy, independent of the data type being copied. If the client only copies integer arrays, only one instance of the template function is generated. If the client copies integer arrays and floating point arrays, the compiler generates two instances of the template function: one specific for integers and another specific for floats.
For this to work, the client calling the function must specify all type dependencies so that the compiler knows which instance of the template function to generate. This can be done through actual arguments where the compiler can deduce their types implicitly or by specifying the types explicitly within angle brackets. Only instances used by the client are actually generated for any particular compilation (except when explicit instantiation is requested as discussed in Section 10.9).
10.2 Specifying Function Templates
Template's Formal Argument List: Specifies the type identifiers used to represent the type dependencies and non-type identifiers used to represent specific values.
Function templates follow the same definition and declaration rules as non- template functions with some additional syntax. We begin with the keyword template followed by the template's formal argument list inside of angle brackets (< >). This is followed by the function's header (i.e., the function's return type, name, and argument list). The signature of a function template is the function's signature, its return type, along with the template's formal argument list.
The template's formal argument list (not to be confused with the function's formal argument list) can be comprised of both type and non-type identifiers. There must be at least one template formal argument specified within this list. Type identifiers represent type dependencies and non-type identifiers represent specific values (both are discussed in the following sections). Each type identifier must be preceded by the keyword class or typename. These identifiers can be used as the data types for the function's formal argument, as the return type, or as the data type for local variables in this function. The identifiers selected cannot be the same as the name of the function. And, they cannot be redeclared (e.g., as a local) within the function itself.
Figure 10-7 illustrates a function template's declaration where there are two template formal arguments, both being type identifiers. Notice that there is a comma separated list of template formal arguments, specifying TYPE_ID1 and TYPE_ID2 as the type identifiers. This enables TYPE_ID1 and TYPE_ID2 to be used as the data types for the function's formal argument list, as the return type, or as the data type of locals within the function's definition.
Figure 10-7: Function Template Declaration
Figure 10-8 illustrates a function template's declaration where there one type identifier and one non-type identifier specified in the template formal argument list. Once a type is specified within this list, it may be used as the data type for any subsequent arguments (e.g., even as the type for non-type identifiers!). In this example TYPE can be used as the data types for the function's formal argument list, as the return type, or as the data type of locals within the function's definition. And, VALUE can be used as a constant within the function itself.
Figure 10-8: Function Template Declaration with Type and Non-Type Arguments
Table 10-2 summarizes the steps necessary to specify a function template.
Steps to Specifying a
Function Template
Step 1: Determine what type dependencies exist.
Step 2: Start both the function's definition and declaration(s) with the template keyword.
Step 3: Follow the template keyword by the identifiers that represent each type dependency and each non-type value. Type dependencies must be preceded by the keyword class or typename and placed inside of angle brackets (< >). Non-type values must be preceded by their data type and optionally the keyword typename.
Step 4: Supply the function's header, as normal.
Step 5: For the function template declaration, end this with a semicolon. For the function template definition, follow this with the function's implementation inside of brackets. The identifiers specified in Step #3 can be used to build in type dependencies for locals.
Table 10-2: Steps to Specifying a Function Template
We can overload template functions with other template and non-template functions. This can be done by defining the same named function with unique argument lists (in their number and/or type).
We can also specify default arguments with a function template, following the rules that we learned in Section 4.5.3. We just need to be careful to avoid ambiguous situations where the signature of more than one function template is the same or where there isn't a best viable choice. While we can specify default arguments to the function, we cannot specify default values to the template's formal argument list.
Practical Rule: The formal argument list of a function template cannot be empty. The name of a template formal argument can appear only once as a type or non-type identifier in a template's formal argument list. However, a template formal argument may be used from its point of declaration to specify the type of subsequent non-type values.
10.3 Template's Formal Argument List – Type Identifiers
The identifiers listed inside the angle brackets are the function template's formal argument list. In this list, we can have one or more identifiers representing data types and values. Those that represent data types are preceded by the keyword class or typename. There is no semantic difference between using either keyword in this context. The following is a template's formal argument list where there is just one type dependency:
template <class TYPE_ID>
or,
template <typename TYPE_ID>
When we have only one identifier listed, the function template depends on only that one type. When we have more than one type listed inside the angle brackets, the function template depends on more than one type. For example, the following is a template's formal argument list where there are two type dependencies:
template <class TYPE_ID1, class TYPE_ID2>
or,
template <typename TYPE_ID1, typename TYPE_ID2>
All or none of the type identifiers can be used as the type of the function's formal arguments. If all are used, the type dependencies can be implicitly deduced from the actual argument types. In this case, a function template call looks the same as a non-template function call. The following example demonstrates this process:
//function template declaration
template <class TYPE_ID>
void function(TYPE_ID formal_arg);
//function call
int i; float f; double* ptr_k;
function(i); //TYPE_ID is deduced to be an integer
function(f); //TYPE_ID is deduced to be a float
function(ptr_k); //TYPE_ID is deduced as a pointer
If all of the type identifiers are not specified as types in the function's formal argument list, the client must explicitly specify the type dependencies inside of angle brackets as part of the function call prior to the function's argument list. The following example demonstrates the changes we would need to make when calling such functions:
//function template declaration
template <class TYPE_ID>
void function(int formal_arg);
//function call
int i;
function<int>(i); //TYPE_ID is an integer
function<float>(i); //TYPE_ID is a float
function <double*>(i); //TYPE_ID is a pointer
Alternatively, a type identifier may be used as the return type of the function. This still requires that the client explicitly specify the type dependencies inside of angle brackets. This is because the compiler cannot deduce the data type of a returned value prior to the function call. The following example demonstrates that our function calls remain the same, even if we change the return type:
//function template declaration
template <class TYPE_ID>
TYPE_ID function(int formal_arg);
//function call
int i; float f; double* ptr_k;
i = function<int>(i); //return type is int
f = function<float>(i); //return type is a float
ptr_k = function <double*>(i); //return type is a pointer
By convention, the identifiers selected as the type dependencies are written in all upper case. Each of the identifiers must be given a unique name. We can't use the same name twice in a function template's formal argument list. But we can use the identifier as many times as needed to represent the data types of formal arguments, local variables, as well as the return type.
Only those identifiers that immediately follow the template's formal argument list can be used within the function. A previous function template's type dependencies are meaningless. The type identifiers hide any identifiers previous used of the same name (i.e., such as globals). The type identifiers are restricted to the scope of the function that immediately follows.
A common programming error is to forget to place the keyword class or typename before every type dependency in the template formal argument.
10.4 Template's Formal Argument List – Non-Type Identifiers
We can also specify non-type identifiers in the template formal argument list. A non-type is used when we want to substitute a value specified by the client at instantiation time. More than one identifier can be listed, each prefaced by its data type. Non-type formal arguments may also be preceded by the keyword typename. For example, the following is a template's formal argument list where there is just one non-type identifier:
template <data_type nontype_identifier>
or,
template <typename data_type nontype_identifier>
The data type of non-type identifiers must be an integral type, an enumeration type, a pointer to an object, a reference to an object, a pointer to a function, a reference to a function, or a pointer to a member. They cannot be void or a floating point type.
Clients must specify the values for non-type identifiers explicitly inside of angle brackets when calling the function. This is because non-type identifiers cannot be used as the data type of a function's formal arguments and therefore cannot be implicitly deduced. The following shows an example of how a client can call a function template with non-type identifiers:
//function template declaration
template <char non_type, class TYPE_ID>
TYPE_ID function(int TYPE_ID);
//function call
int i;
i = function<'\n',int>(i); //non_type is a '\n'
This function can also be called by leaving off trailing types. For example, we can leave off the type dependency because it can be implicitly deduced from the argument and because it is the right most argument. Therefore, the following function call does the same thing:
i = function<'\n'>(i); //non_type is a '\n'
On the other hand, if we had defined the template function with its formal arguments in the opposite order, then this call would not have been valid. In this case, clients would be required to supply both type and non-type values explicitly in angle brackets.
//function template declaration
template <class TYPE_ID, char non_type>
TYPE_ID function(int TYPE_ID);
//function call
int i;
i = function<'\n'>(i); //invalid
i = function<int,'\n'>(i); //valid
Using non-type formal arguments allows the client to specify at compile time some constant expression when calling the function, which can be used by the function to initialize constants, determine the size of statically allocated arrays, or any other initialization. For example, if the non-type identifier is declared to be an integral or enumeration type, the client can specify an integral constant expression as the actual argument. If the non-type identifier is declared to be a pointer to an object, the client can specify an address to a constant expression representing an object of that type. It is only when the non-type identifier is declared as a reference that the client must specify an lvalue expression instead of a temporary.
Non-type formal arguments that are not specified as references are used as a constants when a template function is instantiated. Therefore, it is illegal to attempt to change their value within the function. It is also illegal to take the address of non-type formal arguments that are not references. Think of them as constants; they cannot be modified or taken the address of. The following examples demonstrate legal as well as illegal non-type formal arguments passed by value:
template <char c> //legal
template <class_name* ptr> //legal
template <fctn_name* ptr> //legal
template <float f> //illegal
template <float* ptr> //legal
Non-type formal arguments that are specified as references must correspond to either an object or a function that has external linkage. This means that temporaries cannot be used by client's when they call this function. Only globals with external linkage can be used in such cases. C++ prohibits such non-type formal arguments from being initialized with static globals, locals, or even literals. The following examples demonstrate possible non-type formal arguments passed by reference:
template <class-name& obj> //legal
template <fctn_name& fctn> //legal
10.5 Implicit Deduction when Calling a Template Function
Function templates make sense when functions are the same except for the data type(s) being operated on. For example, copying the contents of an array is the same whether we are working with integers or reals. By implementing such an operation as a function template, we only need to implement the function once and let the compiler generate each instance of the template function based on the type of data that the client uses.
To perform an array copy operation, we have selected two type dependencies just in case the client program tries to copy one type of array to another type. Specifying only one type dependency would prohibit the user from copying one type of array to another. The following is this function template's prototype:
template <class TYPE1, class TYPE2>
void array_copy(TYPE1 dest[], TYPE2 source[], int size);
The following is a complete definition of this function template:
template <class TYPE1, class TYPE2>
void array_copy(TYPE1 dest[], TYPE2 source[], int size) {
for(int i=0; i < size; ++i)
dest[i] = source[i];
}
The client can call this function using any of the following forms:
int int_array1[100], int_array2[100], int_array3[20];
float real_array[100];
char char_array[20];
array_copy(int_array1, int_array2, 100);
array_copy(int_array3, int_array1, 20);
array_copy(real_array, int_array1, 100);
array_copy(int_array1, char_array, 20);
By calling the function template in this way, the compiler generates three different instances of the function for each of the three different combinations of types used. The compiler deduces the data types to be used for TYPE1 and TYPE2 implicitly by the data types of the actual arguments (in the function calls). In one instance TYPE1 and TYPE2 are both integers, in the second instance TYPE1 is a real and TYPE2 is an integer, and in the third instance TYPE1 is an integer and TYPE2 is a character.
10.6 Explicitly Specifying Type Dependencies
Type dependencies are deduced by the compiler by matching the actual arguments in a call with the formal argument in the function template formal argument list. Type dependencies may also be explicitly specified in the function call, adding flexibility in how we specify our formal arguments. By explicitly specifying type dependencies, the return type can be a type distinct from the types in the function's formal argument list.
Type dependencies can be explicitly specified by listing the data types expected for each of the formal template arguments within angle brackets following the name of the function and before the actual argument list:
function_identifier <data type list> (actual arguments)
The data types are matched to the corresponding identifiers listed in the template's formal argument list. If they can be automatically deduced from the actual arguments, then trailing data types can be left out of the list. When a data type is left out, the type of the corresponding argument of the function call is used to deduce the type used in the template function. This is done for each argument. We can explicitly specify both types, or just the first type. However, it is not possible to explicitly specify the type for the second type without specifying the type for the first type as well.
array_copy<int [], int []>(int_array1, int_array2, 100);
array_copy<float []>(real_array, int_array1, 100);
We can have the return type also be based on a type dependency by specifying an additional type in our template's formal argument list. Because a return type cannot be deduced, clients must specify that type explicitly when calling the function. In the following example, this requires that all three type dependencies be specified by the client:
template <class TYPE1, class TYPE2, class TYPE3>
TYPE3 array_copy(TYPE1 dest[], TYPE2 source[], int size) {
for(int i=0; i < size; ++i)
dest[i] = source[i];
}
char a[100];
char b[20];
cin.getline(b, 20, '\n');
//explicitly specify all three type dependencies
int result;
result = array_copy<char [], char [], int> (a,b,strlen(b));
Alternatively, by modifying our function template's definition we can explicitly specify the type dependencies for the return type but leave the other two types up for deduction:
template <class TYPE1, class TYPE2, class TYPE3>
TYPE1 array_copy(TYPE2 dest[], TYPE3 source[], int size) {
for(int i=0; i < size; ++i)
dest[i] = source[i];
}
//client program
char a[100];
char b[20];
cin.getline(b, 20, '\n');
//explicitly specify type dependency for only first type
int result;
result = array_copy<int> (a,b,strlen(b));
When specifying type dependencies, local types, types with no linkage, unnamed types (such as unnamed structures) or compound types with these constructs cannot be used as the actual template arguments for a template function being called. A string literal may also not be used as an actual template argument because it has no internal linkage.
10.7 Specialized Template Functions
Specialization Template Functions: Versions of the function template designed to work with specific data types, rather than generalizations. This allows us to support data types that require specialized operations.
Since all data types cannot be handled in exactly the same manner, we must consider special cases. For example, what happens in the situation where we want to copy an array of character strings? This is different than copying an array of integers or individual characters. Rather than performing a straight copy of the character pointers, the function should produce a copy of each of the character strings. This cannot be accomplished by a simple instantiation of the same function that performs a straight array element copy operation on integers. Here, memory must be allocated and a string copy operation performed for each element in the array. To support this, C++ provides for specialization of our function templates. We will find that it is common to provide specialized versions to handle pointers or other compound data types.
A specialized template function is a function that we implement with the same signature as a template function instantiation. The specialization must be defined after the definition of the function template, but before the first use of the specialization. The definition of the specialized template function must be preceded by the template keyword and followed by an empty set of angle brackets (<>) (i.e., template<>). The specialization will then be used instead of a version that the compiler could instantiate. If a regular C++ function with the same name and signature as a specialized template function is declared before the definition of the function template, that declaration is hidden by the function template or any specialized template function that follows.
The most specialized functions are those who's arguments can be applied to a generalized function template or some other specialization. When calling a function, the most specialized function is used, if one is available that matches the actual argument list. This means that specialization takes precedence followed by generalized function templates.
template <class TYPE1, class TYPE2> //function template
void array_copy(TYPE1 dest[], TYPE2 source[], int size) {
for(int i=0; i < size; ++i)
dest[i] = source[i];
}
template<> //Definition of a specialized template function
void array_copy(char* dest[], char* source[], int size) {
for(int i=0; i < size; ++i) {
dest[i] = new char[strlen(source[i]) + 1];
strcpy(dest[i], source[i]);
}
}
This specialized function is called when the client uses the following:
char saying1[] = "Hello World";
char saying2[] = "This is a great day!";
char* s1[2] = {saying1, saying2};
char* s2[2];
array_copy(s2, s1, 2);
The signature of a specialization template function is the signature of the template function and of the actual template arguments deduced or explicitly specified. Trailing template arguments can be left out when calling a function if they can be implicitly deduced from the call's actual argument types. Template formal arguments that are implicitly deduced by the compiler are not matched through promotion or standard conversions. This means that there must be an exact match. Otherwise, the type dependencies should be explicitly specified by the client calling the function.
We cannot use default template arguments in the definition or declaration of a specialization template function. Specialization functions can be defined as inline code by using the inline keyword after the template <>. This is independent of whether or not the template function is inline.
10.8 Resolving Type Dependencies
When all type dependencies are specified
in the function's formal argument list, they can easily be deduced by the
compiler when resolving function calls. The
first occurrence of an identifier in the function's formal argument list is
used by the compiler to bind the type with the actual argument's type. If that
same type is used again in the argument list, it must be exactly the
same type. This means that if the same type is used more than once in a
function's formal argument list, the first occurrence is used by the compiler
to bind the type. Subsequent occurrences must be exactly the same type.
When a non-type identifier is a pointer to a function, the actual argument must
be an exact match of the formal non-type argument.
It is possible to use one of the type definitions more than once in the function's formal argument list. For example, we could rewrite the function template to copy arrays assuming that the client will always perform copy operations on the same type of data:
template <class TYPE>
void array_copy(TYPE* dest, TYPE* source, int size) {
for(int i=0; i < size; ++i)
dest[i] = source[i];
}
The client might call this function using any of the following:
int int_array1[100], int_array2[100], int_array3[20];
float real_array1[100], real_array2[20];
char char_array1[20];
array_copy(int_array1, int_array2, 100);
array_copy(int_array3, int_array1, 20);
array_copy(real_array2, real_array1, 20);
However, it is illegal for the client to use this function with arrays of different types. In such cases, an error is generated indicating that the use of template function array_copy does not match any of its template definitions. Such an error occurs with either of the following calls:
array_copy(real_array1, int_array1, 100); //ERROR
or,
array_copy(int_array1, char_array1, 20); //ERROR
Remember that all identifiers specified in the template's formal argument list must be specified in the function's formal argument list in order for the compiler to deduce the types from the function call.
10.9 Explicitly Generating Instantiations
We may cause functions to be instantiated explicitly prior to calling the function. This can be done through explicit instantiation, where we declare the function with all template arguments explicitly specified or implicitly deduced (notice that empty angle brackets are redundant). For example, if we had wanted to explicitly instantiate our array_copy function for integer arrays, prior to the function call, we can declare that instantiation of the template function through either of the following three approaches:
//explicitly specified
template void array_copy <int[],int[]>(int[],int[]);
template void array_copy<>(int[],int[]); //implicit deduced
template void array_copy (int[],int[]); //implicit deduced
As with a function call, trailing template arguments can be left out if they can be deduced by the function's formal argument list. The drawback is that a template function can only be explicitly instantiated once, for a particular set of template actual arguments. Therefore, we cannot explicitly instantiate a function more than once with the same type and non-type dependencies.
10.10 Implicitly Generating Instantiations
Whenever the compiler encounters a function call where the function name is the same as a function template, it examines the signature. If there is a specialized function defined with that signature, it will use that version of the function. If not, it will look at the instances of template functions that have already been generated as a result of previous calls. If the signature matches a version that has already been generated, that template function is used. However, if it doesn't match, a new instance of this template function is created to match the newly used signature. If more than one function exists, the compiler selects the function based on the function overloading rules listed in Section 4.5.2.
Practical Rule: Implicit template instantiation is the process of type substitution; it occurs when we call a template function.
10.11 Using Separate Files
It is possible to define our function templates in a separate implementation file and simply declare that those functions exist in the software that uses them. To do so requires that we define or declare our function templates with the export keyword. This tells the compiler that the functions can be used in other "translation units" and may be compiled separately. In such cases the function only needs to be declared prior to invoking the function.
When export is not used, the template function must be defined in every file that implicitly or explicitly instantiates that template function. The good news is that by exporting our function templates, we can simply declare them in other translation units rather than provide complete definitions. Functions exported and declared to be inline are just taken to be inline and are not exported.
The following example demonstrates how this can be done by specifying three separate files. One for the main set of functions, another that specifies the function template declarations, and a third where the function templates and any specializations are implemented. Notice the use of the export keyword.
//main.cpp
//main set of functions
#include "t_func.h"
main() {
t_funct<int, float> (); //template function call
}
//t_func.h
//declarations of the function template(s)
template<class TYPE_ID1, class TYPE_ID2>
void t_funct(TYPE_ID1, TYPE_ID2);
//t_func.cpp
//implementation of the function template(s)
export template<class TYPE_ID1, class TYPE_ID2>
void t_funct(TYPE_ID1 arg_1, TYPE_ID2 arg_2) {...}
By specifying our template functions in separate compilation files, client programs need only include the file that contains the declarations to call the functions and cause instantiations to be generated. If the keyword export was not used, the definition of the function must be within the file where we invoke the function. This would require that we either implement all of our software within a single module (which is ridiculous!) or that we include the implementation file in our client code (e.g., main.cpp could include t_func.cpp in this case, where template functions are treated the same as inline functions).
So when should we use export and when should we simply include our function template implementations in our client code? When we have large functions or when their are many type dependencies or specializations, it is often easier to debug our code by using separate compilation units and use the exporting process. On the other hand, if we have small function templates, few type dependencies, and limited (to no) specialized templates, then there may be no advantage to compiling them separately.
10.12 Caution!
One of the most serious drawbacks of function templates is the danger of generating an instance of the function that is incorrect because of type conflicts. Except by writing specialized template functions to take care of such cases, there is no way to protect the client from using the function incorrectly, causing erroneous instances of the function to be generated. Be very careful when writing function templates to ensure that all possible combinations will create correct functions.
Realize that when we write a function template we may not know the types that will be used by the client. Therefore, we recommend using type dependencies only once within the function's formal argument list. Also, make sure to handle the use of both built-in and pointer types. This may mean that we provide overloaded generalized function templates or template function specializations.
10.13 A Quicksort Function Template
Now that we have introduced function templates, it is time to show how we can apply this to a quicksort function. In this example, two different instantiations of the function are implicitly created by the compiler: one for integers and the other for doubles. All function calls assume that the compile can implicitly deduce the type dependencies from the actual arguments supplied. Notice how the client's function calls appear as natural as if we were not using function templates! A great goal to strive for.
//main.cpp (Ex0425)
#include <iostream>
using namespace std;
#include "qsort.h"
int ia[]={46, 28, 35, 44, 15, 22, 19 };
double da[]={46.5, 28.9, 35.1, 44.6, 15.3, 22.8, 19.4};
int main() {
const int isize=sizeof(ia)/sizeof(int);
const int dsize=sizeof(da)/sizeof(double);
qsort(ia, 0, isize-1); // integer qsort
for(int i=0; i < isize; ++i)
cout << ia[i] << endl;
qsort(da, 0, dsize-1); // double qsort
for(i=0; i < dsize; ++i)
cout << da[i] << endl;
return (0);
}
//qsort.h (Ex0425)
template <class TYPE>
inline void swap(TYPE v[], int i, int j) {
TYPE temp;
temp = v[i];
v[i] = v[j];
v[j] = temp;
}
template <class TYPE>
void qsort(TYPE v[], int left, int right) {
if (left < right) {
swap(v, left, (left+right)/2);
int last = left;
for (int i=left+1; i <= right; ++i)
if (v[i] < v[left])
swap(v, ++last, i);
swap(v, left, last);
qsort(v, left, last-1);
qsort(v, last+1, right);
}
}