Skip to content

Instantly share code, notes, and snippets.

@jdtournier
Last active February 11, 2023 07:26
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jdtournier/2afaeac3b94bf2fe1dc9160d4a61356c to your computer and use it in GitHub Desktop.
Save jdtournier/2afaeac3b94bf2fe1dc9160d4a61356c to your computer and use it in GitHub Desktop.
short demo of explicit instantiation of templates, and avoiding parsing the full definition in every compilation unit

A short demo showing the explicit instantiation of templates, and how to avoid parsing the full definition(s) in every compilation unit. The limitation is that it will only be possible to use the specific specialisations explicitly instantiated in the cpp file.

#include <typeinfo>
#include "class.h"
// Here, the definitions for these class calls are actually in the cpp file.
// Typically these would reside within the class declaration.
// The impact of this is that it will not be possible to instantiate
// this class with other types not listed in the explicit instantiations
// below.
template <class T> Class<T>::Class ()
{
std::cout << "in constructor for " << typeid(T).name() << "\n";
}
template <class T> void Class<T>::method ()
{
std::cout << sizeof(T) << "\n";
}
// explicit instantiations for given types:
template class Class<int>;
template class Class<unsigned int>;
template class Class<float>;
template class Class<double>;
#ifndef __class_h__
#define __class_h__
#include <iostream>
// class declaration
// Note: constructor and method() call are not defined here,
// but in the cpp file. This means the compiler will not have to
// parse and validate the code in these functions for every translation
// unit - but with the limitation that only the specific specialisations
// listed in the corresponding cpp file can be used.
template <class T> class Class
{
public:
Class ();
void method ();
protected:
T obj;
};
#endif
#!/bin/bash
set -ex
g++ -c class.cpp -o class.o
g++ -c main.cpp -o main.o
g++ main.o class.o -o example
./example
nm main.o
nm class.o
#include "class.h"
int main (int argc, char* argv[])
{
// use one of the specialisations explicitly instantiated in class.cpp
// Note that since these are not declared in class.h,
// no duplicate code will be injected into main.o
Class<double> C;
C.method();
return 0;
}
@jdtournier
Copy link
Author

jdtournier commented Mar 9, 2017

On my system, output is:

$ ./compile.sh 
+ g++ -c class.cpp -o class.o
+ g++ -c main.cpp -o main.o
+ g++ main.o class.o -o example
+ ./example
in constructor for d
8
+ nm main.o
                 U __cxa_atexit
                 U __dso_handle
                 U _GLOBAL_OFFSET_TABLE_
00000000000000bc t _GLOBAL__sub_I_main
0000000000000000 T main
                 U __stack_chk_fail
0000000000000062 t _Z41__static_initialization_and_destruction_0ii
                 U _ZN5ClassIdE6methodEv
                 U _ZN5ClassIdEC1Ev
                 U _ZNSt8ios_base4InitC1Ev
                 U _ZNSt8ios_base4InitD1Ev
0000000000000000 r _ZStL19piecewise_construct
0000000000000000 b _ZStL8__ioinit
+ nm class.o
                 U __cxa_atexit
                 U __dso_handle
                 U _GLOBAL_OFFSET_TABLE_
000000000000005a t _GLOBAL__sub_I_class.cpp
0000000000000000 t _Z41__static_initialization_and_destruction_0ii
0000000000000000 W _ZN5ClassIdE6methodEv
0000000000000000 W _ZN5ClassIdEC1Ev
0000000000000000 W _ZN5ClassIdEC2Ev
0000000000000000 n _ZN5ClassIdEC5Ev
0000000000000000 W _ZN5ClassIfE6methodEv
0000000000000000 W _ZN5ClassIfEC1Ev
0000000000000000 W _ZN5ClassIfEC2Ev
0000000000000000 n _ZN5ClassIfEC5Ev
0000000000000000 W _ZN5ClassIiE6methodEv
0000000000000000 W _ZN5ClassIiEC1Ev
0000000000000000 W _ZN5ClassIiEC2Ev
0000000000000000 n _ZN5ClassIiEC5Ev
0000000000000000 W _ZN5ClassIjE6methodEv
0000000000000000 W _ZN5ClassIjEC1Ev
0000000000000000 W _ZN5ClassIjEC2Ev
0000000000000000 n _ZN5ClassIjEC5Ev
0000000000000000 W _ZNKSt9type_info4nameEv
                 U _ZNSolsEm
                 U _ZNSt8ios_base4InitC1Ev
                 U _ZNSt8ios_base4InitD1Ev
                 U _ZSt4cout
0000000000000000 r _ZStL19piecewise_construct
0000000000000000 b _ZStL8__ioinit
                 U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
                 U _ZTId
                 U _ZTIf
                 U _ZTIi
                 U _ZTIj

Note that all desired versions of the Class<X>::method() call are available in the class.o object, but none appear in the main.o object - they are only referenced: symbol _ZN5ClassIdE6methodEv, aka void Class<double>::method(), is specified as undefined (U). In fact, since Class<X>::method() is merely declared in class.h, but only defined in class.cpp, there is no way the compiler could generate the code for those functions when compiling main.cpp - just what we want. 😁

@fteicht
Copy link

fteicht commented Nov 9, 2020

Hi and thanks a lot for this meaningful minimal example!
One question though from my side: whether I comment out or not lines 26 to 29 of file class.h produces the same result on my computer. For me it makes sense because the code of Class' methods are defined outside class.h, thus independently from the extern declarations their definition cannot be included in main.o. Am I missing something? Thanks!

@jdtournier
Copy link
Author

To be honest, I think my code example doesn't actually show the use of extern templates at all... I'd put that together when I was experimenting with them, and it's now clear to me that this isn't how they're supposed to be used.

You're completely right that the extern lines in class.h can be skipped altogether and it won't make any difference. This example shows how you can force instantiation of only those specialisations you know you will need in your code, without forcing every compilation unit to parse the full corresponding definitions: the header only includes the declaration, the full definition (and explicit instantiations) are in the corresponding cpp files, so will be available at link time, and the corresponding code will only be present in that one object file. That's what I was trying to achieve at the time. But that doesn't actually rely on the extern keyword at all.

The extern keyword is only useful if the full definition is also in the header file, and serves to signal to the compiler that code does not need to be generated for that compilation unit, and so does not need to be included in the resulting object file. The idea is to avoid duplicating the work associated with generating the code for every compilation unit that include that header, as well as reducing the size of the resulting object files. Basically, it only matters to prevent the compiler from generating the code in those cases where it would otherwise have done so. You then need to make sure one of your compilation units does force an explicit instantiation, so that at least one version of the code is available at link time.

There's a better explanation of this on this StackOver post. In the meantime, I'll amend my example to avoid giving the wrong impression... Thanks for pointing this out!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment