Part XIII. Explicit template instanciation

This document describes explicit template instanciation (ETI) and how it is implemented in Kicksey-Winsey.

1. Introduction

ETI is a last resort that should be used only in case of severe compiler overhead. Sometimes it cannot be escaped because of hardware memory limitations. It consists in breaking down the code into smaller pieces that are brought together only at link time. The major counterpart is that one needs to tell explicitly to the compiler how to instanciate template classes, which basically amounts to abandoning code genericity. There are already several tutorials on the web that explain how to do this, but it took me enough time to work it out so that I feel it is worth explaining in detail here.

2. Generic tutorial about ETI

Consider a template class defined in the following way in the include file A.hpp:

#include<iostream>
template<class T>
class A
{
	static apply(const T& t) 
	{ // <-- member function definition has to be included in header file !
		std::cout << t << std::endl; 
		// ... further stuff ...
	}
};	
	

Imagine that we are using only A<int> and A<double> in our code, for example with the following main.cpp:

#include "A.hpp";
int main()
{
	A<int>::apply(2);
	A<double>::apply(2.0);
	return 0;
}
	

Each time the compiler encounters class A, it will automatically instanciate either A<int> or A<double>. Then at link time it manages in some elaborated way to get rid of redundant instances and everything falls back into place as if you had defined two separate classes.

Obviously, for this to work, you need to include the definitions for all methods in class A within the header file. This has at least three nasty consequences :

  • each time you change a line of code in the implementation of A, every compilation unit that uses it has to be recompiled,
  • the compiler has to work a lot to keep track of all instances of A, especially when doing inlining,
  • your code does not look clean since it mixes declarations and definitions

The third issue can be partially resolved by imposing the following kind of pattern in all templated header files:

#include<iostream>
template<class T>
class A
{
	inline static void apply(const T& t) ; // only include declaration in class body
};	

/**********************************************
 * BEGIN INLINE/TEMPLATE FUNCTION DEFINITIONS *
 **********************************************/

template<class T>
void
A<T>::apply(const T& t)
{ // <-- member function definition included in header file but not in class body !
	std::cout << t << std::endl; 
	// ... further stuff ...
}
	

In this way, code is much more readable and also easier to document, and there is no cost in terms of genericity. Note that we insert a line feed after the function's return type, contrary to most common formatting schemes. This seemed better because the return type must be fully qualified when defining the function outside of the class body, which leads sometimes to very complicated types ("typename blablabla::type").

Although it seems we have made some progress, we have not solved the efficiency-related issues. To do this, the first step is to move the function definition in another file, which we call A.def.hpp. It may seem strange to keep the suffix ".hpp" for a file containing only definitions and no declarations, but you will see that we shall have to #include it later.

Here is how it looks :

	
#ifdef SC_TEMPLATE_EXPLICIT_INSTANCIATION
#include "A.hpp"
#endif

/**********************************************
 * BEGIN INLINE/TEMPLATE FUNCTION DEFINITIONS *
 **********************************************/

template<class T>
void
A<T>::apply(const T& t)
{ // <-- member function definition included in header file but not in class body !
	std::cout << t << std::endl; 
	// ... further stuff ...
}
	

And the file A.hpp becomes :

#include<iostream>
template<class T>
class A
{
	inline static void apply(const T& t) ; // only include declaration in class body
};	

/**********************************************
 * BEGIN INLINE/TEMPLATE FUNCTION DEFINITIONS *
 **********************************************/

#ifndef SC_TEMPLATE_EXPLICIT_INSTANCIATION
#include "A.def.hpp"
#endif
	

Let us now consider what happens depending on whether or not the macro SC_TEMPLATE_EXPLICIT_INSTANCIATION is defined. If it is not defined, the file A.hpp includes A.def.hpp and thus behaves just as if hadn't split it in two. This allows us to work exactly as before, keeping the full genericity of the code. In that case we should of course not include A.def.hpp anywhere else.

Now if on the contrary SC_TEMPLATE_EXPLICIT_INSTANCIATION is defined, A.def.hpp is not included anymore. If we don't do anything else, the definitions of the template functions have simply vanished. This is perfectly legal in the C++ standard, as long as we are only compiling, and not yet linking. The compiler will generate hanging references to the correct class instances, and defer their resolution to link-time. If you then try to link the application, you will get undefined reference errors, in our case to A<int>::apply(const int&) and to A<double>::apply(const double&).

To avoid these errors, we have to include the definitions of these functions somewhere else. For this, create a file named eti.cpp, and write:

#include "A.def.hpp"

template class A<int>;
template class A<double>;

	

Now if you link main.o together with eti.o, you should be OK. You can then amuse yourself by modifying A.def.hpp, in which case you will see that you only need to recompile eti.cpp, and not main.cpp.

3. Implementation of ETI in Kicksey-Winsey

The implementation follows exactly the same pattern that was outlined for the simple example above.

To facilitate activation of ETI at compile time, a Boost.Build composite feature "eti" has been defined, of which the possible values "on" or "off" have obvious meanings. Activating ETI will have for main consequence to define various macros similar to the SC_TEMPLATE_EXPLICIT_INSTANCIATION that was discussed above, and that will cause some of the modules to be compiled separately, thus decreasing compiler overhead.

Thus you will find that some of the template definitions have been moved to a corresponding .def.hpp file, but that is not the case for all header files. There is no sharp rule determining which ones are split and which are not. Splitting was just suddenly decreeted whenever files tended to grow too big.

Each module where some ETIs have been predefined contains a subdirectory called "eti". This directory is meant to contain ETIs and nothing else.

Currently, ETIs have been defined and tested only for the 2D Navier-Stokes codes.