Class Template Definition and Declaration

Summary

In this post. I will introduce class templates in C++. Especially, how do we declare a template which generates classes and how do we write definitions in separate source files. I will also dive deeper to present how class templates are compiled.

Conclusion

As usual, I present the conclusions first.
Say the customer will use our class templates by the following codes.

// File "main.cpp"
#include "Inferencer.h"
int main() {
    Inferencer<float> x;
    x.f();
    x.g();
    x.h();
    Inferencer<half> y;
    y.f();
    y.g();
    y.h();
  // ...
}

Solution 1, Explicit Instantiation

We then provide the definitions of Inferencer.

// File "Inferencer.h"
template<typename T>
class Inferencer {
public:
    void f();
    void g();
    void h();
};
template<typename T>
inline
void Inferencer<T>::f()
{
  // ...
}

We also provide our declarations as dynamic library libinferencer.so. In which, there are objects of Inferencer<float> and Inferencer<half>.

#include <iostream>
#include "Inferencer.h"
template<typename T>
void Inferencer<T>::g()
{
  std::cout << "Inferencer<T>::g()\n";
}
template<typename T>
void Inferencer<T>::h() {
  std::cout << "Inferencer<T>::h()\n";
}
template class Inferencer<float>;
template class Inferencer<half>;

Note that the two lines at the bottom enforced the compiler to compile Inferencer<float> and Inferencer<half> for the customers. We hide all the codes of our implementations, which is great. However, the customer is only able to use Inferencer<float> and Inferencer<half>. If the customer write Inferencer<double>, linking stages will fail, since there are no objects of Inferencer<double>.

Solution 2, Inclusion

We provide our definitions and declarations together.

// File "Inferencer.h"
template<typename T>
class Inferencer {
public:
    void f();
    void g();
    void h();
};
template<typename T>
inline
void Inferencer<T>::f()
{
  // ...
}
template<typename T>
void Inferencer<T>::g()
{
  std::cout << "Inferencer<T>::g()\n";
}
template<typename T>
void Inferencer<T>::h() {
  std::cout << "Inferencer<T>::h()\n";
}

Then the compiler of the customers gets all details and can generate many customized classes, which is great. But the header contains every detail about our declaration. Customers know everything. And the compilation will also be slower.

Details

I encounter this problem when I need to develop deep learning inferencers for both float and half using CUDA. If you read this post, it is very likely that you meet the same linking problems as I do. That is, the compiler can not find the reference.

1. Class Templates and “Templates Class?”

Actually, there are no templates classes (although you can say template classes are instantiations of class templates, but it is not recommended and confuses people. See this post).

We only have class templates which generate classes. Read class templates from right to left as C++ always does (int const * const int_ptr): “templates to define classes (and functions)”.

Classes templates are not classes. Only after you give them concrete types, you get a real class.

2. Class Templates Are Not Classes

Templates are not like normal classes since that the compiler does not generate object code for a template or any of its members until the template is instantiated with concrete types.

When the compiler encounters a class template instantiation such as Inferencer<float> a for the first time, which means no class with that signature exists yet. The compiler knows it needs to generate a new class. It also attempts to generate code for any member functions that are used.

The compiler begins to search the definitions. If those definitions are in a file that is included, as the second solution does, the compiler is happy and begin to compile the real classes.

If there are no definitions available, from the compiler’s point of view, this isn’t necessarily an error since the functions may be defined in another translation unit, in which case the linker will find them. If the linker does not find that code, it raises an unresolved external error.

3. Force to Compile Class Templates

If you know definitively the set of types that will be used to instantiate a template, you can use solution 1.

In this case, you can separate out the template code into a .h and .cpp file. You should explicitly instantiate the templates in the .cpp file. This will cause object code to be generated that the compiler will see when it encounters user instantiations.

You create an explicit instantiation by using the keyword template followed by the signature of the entity you want to instantiate. This can be a type or a member. If you explicitly instantiate a type, all members are instantiated.

4. One Header

Using solution 2, the compiler has access to the complete template definitions and can instantiate templates on-demand for any type. It is simple and relatively easy to maintain.

However, it does have a cost in terms of compilation times.

In this case, every .cpp file that #includes the header will get its own copy of the function templates and all the definitions. The linker will generally be able to sort things out so that you do not end up with multiple definitions for a function, but it takes time to do this work. This cost can be significant in large programs.

Reference

templates at isocpp.org
templates at cplusplus.com
templates at microsoft

Leave a Reply

Your email address will not be published. Required fields are marked *