Chapter 7: Class Template Instantiation
Implicit instantiation, explicit instantiation, extern templates, and translation unit mechanics
As discussed previously, class templates are designed to generate a particular definition of the class with the specific data type(s) deduced by the compiler from the passed-in arguments during compilation. In this chapter, we will learn various aspects of class template instantiation. Understanding class template instantiation is pivotal to the overall understanding of how templates function in general. The fundamental concept of template instantiation remains the same for both function templates and class templates. However, instantiation of class templates is more complex than function templates.
In this chapter we will cover the following topics:
- Revisiting class template
- Learning about implicit instantiation
- Learning about explicit instantiation
- Learning about extern templates
Technical requirements
Most of the examples in this chapter will work with the standard C++11 compiler. Some of the examples in this chapter will require support of C++14 or higher compilers.
The code files for this chapter can be found on GitHub at: Source Code
Revisiting class template
In the previous chapter, we discussed how template classes are defined and used in a program but did not detail how they are instantiated and when the compiler generates binary code for them. This chapter will go over some essential characteristics of template classes in terms of their instantiation. In the next section, we will learn that the compiler generates no binary code if the template class has not been instantiated — in other words, if no object has been created of the template class, the compiler will not generate any binary code for it.
No binary code if not instantiated
We have seen in template functions that the compiler does not generate any binary code unless the function template has been called in the program. So, if there is any unused template function it doesn’t appear in the generated binary code — this keeps the binary file size optimized. Similarly, the compiler generates no binary code unless at least one object has been created of the template class. Let us have a look at the following class template:
#ifndef __SAMPLE_TEMPLATE_H__
#define __SAMPLE_TEMPLATE_H__
#include <iostream>
template<typename T>
class MyTemplate {
private:
T data;
public:
MyTemplate() = default;
MyTemplate(T val);
T get_val() const;
void print_val();
void print_welcome_message();
~MyTemplate() = default;
};
template<typename T>
MyTemplate<T>::MyTemplate(T val) : data(val)
{
}
template<typename T>
T MyTemplate<T>::get_val() const
{
return data;
}
template<typename T>
void MyTemplate<T>::print_welcome_message()
{
std::cout << "Welcome to Templates" << std::endl;
}
#endif
In this example, we have defined a template class with one template type parameter T. The template class has a member variable called data of type T, and two member functions get_val() and print_val(). Notice that we have defined get_val() but not print_val(). The template class has been declared and defined in a header file which is then included in the following CPP file:
#include "sample_template.h"
int main()
{
std::cout << "Hello World" << std::endl;
return 0;
}
In this file, we have added the template class header file but have not instantiated any class object in main(). As we have not created an instance of MyTemplate, the compiler will not generate any code for the template class. We will compile the program and then use the nm tool to verify this fact:
$ g++ sample_program_1.cpp -o sample_program_1 -Wall -Wextra -Wpedantic -Werror
$ nm -C sample_program_1 | grep -i MyTemplate
$
As you can see, there is no symbol generated as MyTemplate, which means the compiler has not generated any code for the template class.
However, this is not true for non-template classes. Let us define a non-template class and see how it differs:
#ifndef __SAMPLE_TEMPLATE_H__
#define __SAMPLE_TEMPLATE_H__
#include <iostream>
class MyClass {
private:
int data;
public:
MyClass() = default;
MyClass(int val);
int get_val() const;
~MyClass() = default;
};
MyClass::MyClass(int val) : data(val)
{
}
int MyClass::get_val() const
{
return data;
}
#endif
#include "sample_class.h"
int main()
{
std::cout << "Hello World" << std::endl;
return 0;
}
We have included the non-template class MyClass but created no object of it. Let us compile and check whether the string MyClass exists in the binary:
$ g++ sample_program_2.cpp -o sample_program_2 -Wall -Wextra -Wpedantic -Werror
$ ./sample_program_2
Hello World
$ nm -C sample_program_2 | grep -i "MyClass" 2>/dev/null
000000000000095e t _GLOBAL__sub_I__ZN7MyClassC2Ei
00000000000008ba T MyClass::MyClass(int)
00000000000008ba T MyClass::MyClass(int)
00000000000008d2 T MyClass::get_val() const
The string MyClass exists in the binary — the compiler has generated binary code for the non-template class MyClass even though no object was created. This confirms that binary code generation differs for non-template and template classes.
Instantiation at object creation
The template class is instantiated when we create an object of the class. Let us have a look at the following program, which creates an object of the MyTemplate class defined previously:
#include "sample_template.h"
int main()
{
MyTemplate<int> myObj(10);
std::cout << "val = " << myObj.get_val() << std::endl;
return 0;
}
In the main() function, we have created an object named myObj of the MyTemplate template class for type int:
MyTemplate<int> myObj(10);
We have specified the instantiation type as int within the angle brackets and passed a value of 10 to the single parameter constructor. Let us compile, run, and verify with nm:
$ g++ sample_program_3.cpp -o sample_program_3 -Wall -Wextra -Wpedantic -Werror
$ ./sample_program_3
val = 10
$ nm -C sample_program_3 | grep -i "MyTemplate" 2>/dev/null
0000000000000a62 W MyTemplate<int>::MyTemplate(int)
0000000000000a62 W MyTemplate<int>::MyTemplate(int)
0000000000000a7a W MyTemplate<int>::get_val() const
The compiler has generated binary code for the template class MyTemplate, and specifically for the get_val() member function. However, the member function print_val() is not in the listing. In the next section, we will discuss what happened to it.
Unused member function not instantiated
As in the case of template functions, a class member function is not instantiated — no binary code is generated for it — if it has not been called in the program. The nm output from sample_program_3 confirms this: the compiler generated binary code only for get_val() but not for print_val() or print_welcome_message().
Let us modify the program to call print_welcome_message() from main():
#include "sample_template.h"
int main()
{
MyTemplate<int> myObj(10);
myObj.print_welcome_message();
std::cout << "val = " << myObj.get_val() << std::endl;
return 0;
}
$ g++ sample_program_4.cpp -o sample_program_4 -Wall -Wextra -Wpedantic -Werror
$ nm -C sample_program_4 | grep -i "MyTemplate" 2>/dev/null
0000000000000a86 W MyTemplate<int>::print_welcome_message()
0000000000000a6e W MyTemplate<int>::MyTemplate(int)
0000000000000a6e W MyTemplate<int>::MyTemplate(int)
0000000000000abe W MyTemplate<int>::get_val() const
The compiler has now generated code for print_welcome_message(). Now let us call the print_val() function, which has never been defined:
#include "sample_template.h"
int main()
{
MyTemplate<int> myObj(10);
myObj.print_val();
return 0;
}
$ g++ sample_program_5.cpp -o sample_program_5 -Wall -Wextra -Wpedantic -Werror
/tmp/cceIupyB.o: In function `main':
sample_program_5.cpp:(.text+0x30): undefined reference to `MyTemplate<int>::print_val()'
The compiler throws a compilation error because print_val() is not defined. Prior to this point we didn’t call print_val() anywhere in the program, so the compiler ignored the missing definition. Now that it has been referenced, the compiler is looking for it.
However, if the member function has been defined, the compiler will check for unknown symbols and syntactic errors even if it has not been used. Let us look at the following template with intentional errors:
#ifndef __SAMPLE_TEMPLATE_ERROR__
#define __SAMPLE_TEMPLATE_ERROR__
#include <iostream>
template<typename T>
class MyTemplate {
public:
MyTemplate() = default;
void foo();
~MyTemplate() = default;
private:
};
template<typename T>
void MyTemplate<T>::foo()
{
std::cout << var // undeclared symbol, missing semicolon
}
#endif
#include "sample_template_error.h"
int main()
{
MyTemplate<int> myObj;
return 0;
}
We create an object but do not call foo(). Let us try to compile:
$ g++ sample_program_error.cpp -o sample_program_error -Wall -Wextra -Wpedantic -Werror
In file included from sample_program_error.cpp:1:0:
sample_template_error.h: In member function 'void MyTemplate<T>::foo()':
sample_template_error.h:16:18: error: 'var' was not declared in this scope
The compiler complains about the undeclared symbol var. If we declare var as int but leave out the semicolon, the compiler then reports:
$ g++ sample_program_error.cpp -o sample_program_error -Wall -Wextra -Wpedantic -Werror
In file included from sample_program_error.cpp:1:0:
sample_template_error.h: In member function 'void MyTemplate<T>::foo()':
sample_template_error.h:18:1: error: expected ';' before '}' token
In the next section, we will learn about the two different types of class template instantiation: implicit and explicit.
Learning about implicit instantiation
So far, we have seen that if we create an object of a template class, the compiler silently creates an instance of the template class with the specific data type. This type of instantiation is known as implicit instantiation. When creating an object of a template class, two types of instantiation happen:
- class template instantiation, and
- the instantiation of the instantiated class itself — that is, creating an object of the newly instantiated class.
Let us revisit sample_program_3.cpp. When the compiler sees the following statement:
MyTemplate<int> myObj(10);
…it understands that we want to create an instance of the template class MyTemplate<T> for int type data, so it first creates an instance of the template class with int as shown in Figure 7.1:
Once the template has been instantiated with int, the compiler then creates an object of the class using the single parameter constructor with the passed value of 10. That is the second instantiation. The following figure illustrates this two-step process:
In implicit instantiation, we do not have to tell the compiler to do so — it silently instantiates the template class before creating the object. To verify this, we can use the compilation flag -fno-implicit-templates, which disables implicit instantiation:
$ g++ sample_program_3.cpp -o sample_program_3 -Wall -Wextra -Wpedantic -Werror -fno-implicit-templates
/tmp/cc966sKx.o: In function `main':
sample_program_3.cpp:(.text+0x25): undefined reference to `MyTemplate<int>::MyTemplate(int)'
sample_program_3.cpp:(.text+0x47): undefined reference to `MyTemplate<int>::get_val() const'
With implicit instantiation disabled, the linker cannot resolve the symbols and we get linking errors. In the next section, we will see how to explicitly instantiate a template class and how that can be useful in certain situations.
Learning about explicit instantiation
To understand explicit instantiation of template classes, we first need to understand the compilation and linking process in general.
Learning about compilation and linking process
The compiler compiles source code in groups. The group of lines of code compiled together is called the translation unit, which is essentially the content of a single source file with all the header files included. The output of the compilation process is the object file. The compiler hands the object files to the linker. The linker links the object files together, resolves any undefined symbols, and generates the final executable file. The steps involved are shown in Figure 7.3:
In Figure 7.3, we can see two source files — Source1.cpp and Source2.cpp — being compiled into two object files Source1.o and Source2.o, which the linker then combines into the final executable binary. For all practical purposes, a translation unit is the source .cpp file with all its header files included.
Multiple translation units and template class instantiation
In the case of template classes, the compiler instantiates the template class separately in each translation unit. For example, if both Source1.cpp and Source2.cpp reference MyTemplate<T> for the same int data type, then the compiler will instantiate two separate copies of MyTemplate<int> in two different translation units. This is shown in Figure 7.4:
Let us verify this in real code. We define the template class in a header file:
#ifndef __SAMPLE_TEMPLATE_EXPLICIT__
#define __SAMPLE_TEMPLATE_EXPLICIT__
#include <iostream>
template<typename T>
class MyTemplate {
public:
MyTemplate() = default;
MyTemplate(T x);
void print_val();
~MyTemplate() = default;
private:
T val;
};
template<typename T>
MyTemplate<T>::MyTemplate(T x) : val(x) {}
template<typename T>
void MyTemplate<T>::print_val()
{
std::cout << val << std::endl;
}
#endif
📎 sample_program_explicit_1.cpp
#include "sample_template_explicit.h"
void foo()
{
MyTemplate<int> myObj1(10);
myObj1.print_val();
}
📎 sample_program_explicit_2.cpp
#include "sample_template_explicit.h"
extern void foo();
int main()
{
MyTemplate<int> myObj2(20);
myObj2.print_val();
foo();
return 0;
}
Using the -c flag to compile without linking, we can inspect the generated object files:
$ g++ -c sample_program_explicit_1.cpp sample_program_explicit_2.cpp -Wall -Wextra -Wpedantic -Werror
$ ls -la *.o
-rw-r--r-- 1 VBR09 tpl_sky_pdd_jira_user 3752 May 27 16:15 sample_program_explicit_1.o
-rw-r--r-- 1 VBR09 tpl_sky_pdd_jira_user 3816 May 27 16:15 sample_program_explicit_2.o
$ nm -g -C --defined-only *.o
sample_program_explicit_1.o:
0000000000000000 T foo()
0000000000000000 W MyTemplate<int>::print_val()
0000000000000000 W MyTemplate<int>::MyTemplate(int)
0000000000000000 W MyTemplate<int>::MyTemplate(int)
sample_program_explicit_2.o:
0000000000000000 W MyTemplate<int>::print_val()
0000000000000000 W MyTemplate<int>::MyTemplate(int)
0000000000000000 W MyTemplate<int>::MyTemplate(int)
0000000000000000 T main
As you can see, the compiler has instantiated two separate copies of MyTemplate<int> in two different translation units. When the linker processes these object files, it keeps only one copy and discards the other.
Problem with implicit instantiation
The instantiations in each of the source files are implicit instantiations — we do not explicitly tell the compiler to instantiate, but it automatically does so while compiling. The problem is that multiple instances of the template class for the same data type may exist in different translation units. We only have two source files in our example, but imagine the duplication happening across hundreds of source files in a large project. The resulting problems are:
- increased compilation time, and
- increased object file size.
To overcome this, we can resort to explicit instantiation, which we will learn in the next section.
Learning about syntax of explicit instantiation
To avoid redundant instantiations of template classes in multiple source files, we can explicitly instantiate the template class for all required data types in one source file and reuse those instances everywhere else. The following figure shows the basic syntax of explicit instantiation:
The explicit instantiation starts with the keyword template followed by the name of the template class, then the required data type specified within angle brackets. When the compiler sees this statement, it instantiates the template class with the specified data type.
Learning explicit instantiation with example
We will divide the example code into the following logical parts:
- the template class declaration will go into a header file (
*.h) - the template class definition will go in a C++ source file (
*.cpp)
Let us now declare the template class in a header file:
📎 sample_template_declaration.h
#ifndef __SAMPLE_TEMPLATE_2__
#define __SAMPLE_TEMPLATE_2__
#include <iostream>
template<typename T>
class MyTemplate {
public:
MyTemplate() = default;
MyTemplate(T x);
~MyTemplate() = default;
private:
T val;
};
#endif
Now let us implement the member function definitions and explicitly instantiate the template class in a separate .cpp file. The compiler will create instances for each of the explicitly declared data types when compiling this file:
📎 sample_template_definition.cpp
#include "sample_template_declaration.h"
template<typename T>
MyTemplate<T>::MyTemplate(T x) : val(x) {}
template class MyTemplate<int>;
template class MyTemplate<char>;
After the member function definition, we have explicitly instantiated two different versions of the template class — one for int and one for char. Now let us write the main program:
📎 sample_program_explicit_inst.cpp
#include "sample_template_declaration.h"
int main()
{
MyTemplate<int> myObj(10);
MyTemplate<char> myObj2('A');
return 0;
}
It is important to note that the compiler has no access to the definition of MyTemplate<T> while compiling this file — the header file contains only the declaration. Consequently, when the compiler encounters the references to the template it cannot instantiate the class:
MyTemplate<int> myObj(10);
MyTemplate<char> myObj2('A');
Let us generate and inspect the object files:
$ g++ -c sample_program_explicit_inst.cpp -o sample_program_explicit_inst.o -Wall -Wextra -Wpedantic -Werror
$ nm -g -C --defined-only sample_program_explicit_inst.o
sample_program_explicit_inst.o:
0000000000000000 T main
$ g++ -c sample_template_definition.cpp -o sample_template_definition.o -Wall -Wextra -Wpedantic -Werror
$ nm -g -C --defined-only sample_template_definition.o
0000000000000000 W MyTemplate<char>::MyTemplate(char)
0000000000000000 W MyTemplate<int>::MyTemplate(int)
The compiler has only instantiated the template class in sample_template_definition.o for int and char. Finally, let us compile both source files together and link them:
$ g++ sample_template_definition.cpp sample_program_explicit_inst.cpp -o sample_program_explicit_inst -Wall -Wextra -Wpedantic -Werror
$ ./sample_program_explicit_inst
The compiler didn’t throw any error even though it had no visibility of the template class definition in sample_program_explicit_inst.cpp. The compiler knows a template class named MyTemplate<T> exists from the declaration in the header file, so it defers symbol resolution until the linking stage. The linker then resolves the unknown symbols by cross-referencing both object files. This is how explicit instantiation overcomes the problem of multiple duplicate instantiations.
Learning about extern templates
In the previous example, we had only declared the template class in the header file, not defined it. If the complete definition of the template class is available in the same translation unit, then the compiler must instantiate the template class if there is a reference to it. So, if the header file contains both the template class declaration and definition, the compiler is obliged to instantiate the template class in every translation unit where that header is included.
To avoid this, C++11 introduced extern templates. Using an extern template, you can force the compiler not to instantiate the template class in the current translation unit. Let us define a template in the following header file:
#ifndef __SAMPLE_TEMPL_HEADER__
#define __SAMPLE_TEMPL_HEADER__
template<typename T>
class MyTemplate {
public:
MyTemplate() = default;
MyTemplate(T x);
T get_val();
~MyTemplate() = default;
private:
T val;
};
template<typename T>
MyTemplate<T>::MyTemplate(T x) : val(x) {}
template<typename T>
T MyTemplate<T>::get_val() { return val;}
#endif
Now we include this header in one .cpp file and explicitly instantiate the template class for int and char:
#include "sample_templ_header.h"
template class MyTemplate<int>;
template class MyTemplate<char>;
In the main program, we include the same header file and use the extern keyword to declare that the instantiations exist elsewhere, forcing the compiler not to instantiate the template class in this translation unit:
#include "sample_templ_header.h"
extern template class MyTemplate<int>;
extern template class MyTemplate<char>;
int main()
{
MyTemplate<int> obj1(10);
MyTemplate<char> obj2('A');
return 0;
}
We can verify using nm that the template class is only instantiated once:
$ g++ -c sample_main.cpp -o sample_main.o -Wall -Wextra -Wpedantic -Werror
$ g++ -c sample_source.cpp -o sample_source.o -Wall -Wextra -Wpedantic -Werror
$ nm -g -C --defined-only *.o
sample_main.o:
0000000000000000 T main
sample_source.o:
0000000000000000 W MyTemplate<char>::get_val()
0000000000000000 W MyTemplate<char>::MyTemplate(char)
0000000000000000 W MyTemplate<char>::MyTemplate(char)
0000000000000000 W MyTemplate<int>::get_val()
0000000000000000 W MyTemplate<int>::MyTemplate(int)
0000000000000000 W MyTemplate<int>::MyTemplate(int)
The template class has only been instantiated once — in sample_source.o. The final binary can be produced with:
$ g++ sample_source.cpp sample_main.cpp -o sample_main -Wall -Wextra -Wpedantic -Werror
We are discussing various mechanisms of template class instantiation in this chapter. Choosing one over the other depends on the application for which you are defining the template. Depending on usage, one mechanism may be better suited over the other. Making yourself familiar with each of these mechanisms will make it easier to decide which to apply in a given situation.
Summary
In this chapter, we dived deep into the aspect of template class instantiation. The chapter begins with some familiar concepts of template instantiation that we learned in Chapter 2 while discussing function templates. As the chapter progressed, we gradually moved to the nitty-gritty of template instantiation specific to class templates.
If you have followed the chapter carefully, you should understand when and how the template class and its member functions are instantiated. You should also have an understanding of implicit and explicit instantiations. This chapter also touches upon the fundamental concepts of C++ source file compilation, object file creation, linking, and executable binary file creation for completeness.
We also discussed the creation of multiple instances of the same template class in different translation units due to implicit instantiation. You should have noticed that explicit instantiation can help reduce compilation time and object file size. You should also have a fair understanding of what an extern template is and how it helps reuse a previously instantiated template class.
In the next chapter, we will learn about the inheritance of a template class and friend of a template class.
Questions & Answers
- No binary code is generated for a C++ class if no object has been created of that class in the program. [True/False]
Ans: False - No binary code is generated for a template class if no object of that class has been created. [True/False]
Ans: True - If a member function of a template class is not called anywhere in the program, the member function is not instantiated. [True/False]
Ans: True - Template class instantiation happens as soon as the compiler sees the definition of the template class in the source code. [True/False]
Ans: False - In implicit instantiation we must use the keyword
templateto force the compiler to do the template class instantiation. [True/False]
Ans: False - The problem of implicit instantiation is that there is a possibility of increased compilation time and object file size. [True/False]
Ans: True - To avoid the problem of redundant template instances and increased compilation time, we can use explicit instantiation of the template class. [True/False]
Ans: True - Extern templates are useful if both the declaration and definition of the template class are available in the header file. [True/False]
Ans: True - Object files are the output of the compilation process, not the linking process. [True/False]
Ans: True - Unknown symbols are resolved during the compilation process, and in the linking process only different object files are linked together. [True/False]
Ans: False
3 Comments