Using C++ Templates¶
Note
Changes to this document must be approved by the System Architect (RFC-24). To request changes to these policies, please file an RFC.
Introduction¶
The classical use of templates is well illustrated by examples such as vector in the C++ STL.
The entire definition of the template must be available to the compiler in order to handle code that instantiates LSST types such as std::vector<DataProperty>
.
The case of many of the LSST framework classes is quite different.
We have a templated class Image<typename T>
, but we do not expect users to instantiate a wild and wooly zoo of Images
; a type such as Image<DataProperty>
makes no sense (and won’t compile).
As a consequence, we can move the definitions of much of the implementation of these classes out of the header files, which produces at least three desirable effects:
- Compile times are shorter.
- Implicit dependencies are (sometimes greatly) reduced, as much less of the supporting structure for the classes is exposed to user code that includes e.g.
lsst/fw/Image.h
. - Self-contained (even shared) libraries can be built.
For true container classes like the STL that require full definitions, we will still place the definitions in separate files, but these implementation files will be included in the header files. In the absence of compiler options this leads to code duplication, but we will live with it until the overhead, similar to that of inline functions, proves excessive.
Explicit instantiation¶
For cases such as Image
we will use explicit instantiation.
A complete example is here, and consists of three source files (Foo.h, Foo.cc and main.cc) and an SConstruct file.
foo.h looks something like this:
template <typename T>
class Foo {
public:
explicit Foo();
explicit Foo(T x);
~Foo();
inline T getVal() const;
void setVal(T val);
private:
T _val;
See below for the full source.
You’ll notice that the class is defined, but none of its members are.
In main.cc,
#include <iostream>
#include "Foo.h"
int main() {
Foo<int> i;
Foo<float> x(1.234);
std::cout << "i = " << i.getVal() << ", " << x.getVal() << std::endl;
i.setVal(10);
std::cout << "i = " << i.getVal() << std::endl;
std::cout << "mySizeof(float) = " << mySizeof<float>() << std::endl;
}
Foo
, without any knowledge of its implementation.
This is a good thing; but how was it achieved? Look at Foo.cc:
template <typename T>
Foo<T>::Foo() : _val(0) {}
template <typename T>
Foo<T>::Foo(T val) : _val(val) {}
template <typename T>
Foo<T>::~Foo() {}
template<typename T> T Foo<T>::getVal() const; // inline in Foo.h
template <typename T>
void Foo<T>::setVal(T val) {
template <typename T>
Here are the missing method definitions, along with the lines
//
which tell the compiler to generate the classes Foo<float>
and Foo<int>
; exactly the classes that main.cc uses.
More explicit instantiation¶
If you look at Foo.h you’ll see that there’s more code than discussed above.
The main features are that an inline member of Foo
needs to be fully specified in Foo.h (how else could the compiler inline it?), and also an example of explicit instantiation of a function, rather than a class.
There should be no surprises.
What about extern template
and -no-implicit-templates
?¶
These code examples didn’t use either extern template
or -no-implicit-templates
; why not?
The former, extern template
is not (yet) in the C++ standard (but see C++ proposal N1987), while the latter is a g++ command line flag.
What would we gain by using them?
The example C++ in the previous section didn’t give the compiler a choice; even if it wanted to instantiate Foo<int>
in main.cc it simply didn’t know enough.
However, if we’d included the full template implementation in Foo.h, matters would have been different; exactly what happens isn’t specified by the C++ standard as you cannot tell what the compiler decided, but many compilers (including g++) generate a definition of the class and its members in each object file, and then use the linker to remove duplicates.
It is this behavior that is modified by extern template
and -no-implicit-templates
; they instruct the compiler not to instantiate Foo<int>
even if it could.
How do I know what it did? Well, the most direct way (on a command line) is to say:
nm main.o | c++filt | grep 'Foo(float)'
which results in:
U Foo<float>::Foo(float)
That is, the constructor Foo<float>::Foo
isn’t defined in main.cc.
If you move the definition of the constructor from Foo.cc to Foo.h and repeat the experiment, you’ll get:
W Foo<float>::Foo(float)
unless you add an extern template Foo<float>
or compile with -no-implicit-templates
.
Legalities¶
Careful reading of Stroustrup implies that this technique is legal.
The relevant passage is Section 13.7 (p. 351), which is talking about the use of the export
keyword.
We’re not using export
, but the compiler doesn’t know that when it compiles out.h
:
// out.h:
template <class T> void out(const T& t);
// user1.c:
#include "out.h"
// use out()
Template usage example¶
Sconstruct¶
# -*- python -*-
#
# Setup our environment
#
import lsst.SConsUtils as scons
env = scons.makeEnv("Foo",
r"$HeadURL$",
[])
env.SharedLibrary('Foo', "Foo.cc")
env.Program(["main.cc"], LIBS="Foo", LIBPATH=".")
Foo.h¶
#if !defined(FOO_H)
#define FOO_H 1
#include <cstddef>
template <typename T>
class Foo {
public:
explicit Foo();
explicit Foo(T x);
~Foo();
inline T getVal() const;
void setVal(T val);
private:
T _val;
};
//
// Inlined functions need to be in the .h file (maybe in the class definition itself)
//
template <typename T>
inline T Foo<T>::getVal() const {
return _val;
}
//
// Declare a templated function
//
template <typename T>
size_t mySizeof(void);
//
// Provide a declaration for explicitly-instantiated version[s] of mySizeof()
// This is only needed if we provide enough information here for the compiler
// to instantiate the function/class
//
// It's non-compliant code (but see C++ standards proposal N1897), and stops
// the compiler instantiating the function in each file, and leaving it up
// to the linker to clean up the mess
//
#if 0
extern template size_t mySizeof<float>(void);
#endif
#endif
Foo.cc¶
/// Implementation of templated class Foo
#include "Foo.h"
template <typename T>
Foo<T>::Foo() : _val(0) {}
template <typename T>
Foo<T>::Foo(T val) : _val(val) {}
template <typename T>
Foo<T>::~Foo() {}
#if 0
template<typename T> T Foo<T>::getVal() const; // inline in Foo.h
#endif
template <typename T>
void Foo<T>::setVal(T val) {
_val = val;
}
//
// And a templated function
//
template <typename T>
size_t mySizeof() {
return sizeof(T);
}
//
// Explicit instantiations
//
template class Foo<float>;
template class Foo<int>;
template size_t mySizeof<float>();
main.cc¶
#include <iostream>
#include "Foo.h"
int main() {
Foo<int> i;
Foo<float> x(1.234);
std::cout << "i = " << i.getVal() << ", " << x.getVal() << std::endl;
i.setVal(10);
std::cout << "i = " << i.getVal() << std::endl;
std::cout << "mySizeof(float) = " << mySizeof<float>() << std::endl;
}