C++ / TEMPLATES AND GENERIC PROGRAMMING
Class templates and their instantiation
Write your own class templates and predict when the compiler instantiates each one, member by member, and where its definitions must live.
What you will learn
- Instantiate a class template by naming it with arguments, as in Box<int> b(21);
- Treat Box<int> and Box<double> as unrelated types with independent static members
- Rely on lazy instantiation: an unused member need not be valid for every T
- Keep templates in headers, or emit them once with template class Box<int>;
Understanding Class templates and their instantiation
A class template is a set of instructions for making classes, not a class itself. Writing template <typename T> class Box { ... }; produces no machine code at all; the compiler only acts when you name a specialisation such as Box<int>, at which point it substitutes int for T and builds a genuinely new type. Box<int> and Box<double> are then as unrelated as int and std::string: separate member functions, separate static data members, and no conversion between them. Arguments need not be types either, so a non-type parameter like std::size_t N bakes a constant into the type and makes Buffer<int, 3> and Buffer<int, 4> two different classes with different sizes.
That generation is lazy and proceeds member by member. Naming Box<int> instantiates the member declarations, which is enough to fix the layout, the size, and every signature, but the body of a member function is only instantiated when something uses that member. This is why value_ * 2 inside print_doubled is never even checked for Box<std::string>: nothing calls it, so nothing compiles it. The same rule is why std::vector<T> works with a type that has no default constructor right up until you call resize(n), whose body is the part that needs one.
Because instantiation happens wherever a specialisation is named, the full definition has to be visible at that point, which is why class templates live in headers instead of being split into a .cpp the way an ordinary class is. Each translation unit that mentions Box<int> instantiates its own copy and the linker collapses the duplicates, so the final binary holds one. When you want that copy emitted in exactly one place, an explicit instantiation definition, template class Box<int>;, tells the compiler to generate every member of that specialisation right there, which lets the member bodies stay in a single .cpp.
<iostream>
<string>
template <typename T>
class Box {
public:
explicit Box(T v) : value_(v) { ++instances; }
const T& get() const { return value_; }
// Only compiled if it is actually called for a given T.
void print_doubled() const { std::cout << value_ * 2 << '\n'; }
static int instances;
private:
T value_;
};
template <typename T>
int Box<T>::instances = 0; // one counter per instantiation
int main() {
Box<int> a(21);
Box<int> b(2);
Box<std::string> s("hi"); // print_doubled is never instantiated here
a.print_doubled();
b.print_doubled();
std::cout << s.get() << '\n';
std::cout << Box<int>::instances << ' '
<< Box<std::string>::instances << '\n';
}
A class template is a pattern rather than a class: the compiler builds one distinct class per distinct argument list, and compiles each member's body only when that member is used.
Worked examples
A non-type parameter is part of the type
Shows that changing only the constant argument produces a different, independent class.
<cstddef>
<iostream>
<type_traits>
template <typename T, std::size_t N>
struct Buffer {
T data[N];
static constexpr std::size_t size() { return N; }
};
int main() {
Buffer<int, 3> small{{1, 2, 3}};
Buffer<int, 4> big{{1, 2, 3, 4}};
std::cout << small.size() << ' ' << big.size() << '\n';
std::cout << std::boolalpha
<< std::is_same_v<Buffer<int, 3>, Buffer<int, 4>> << ' '
<< std::is_same_v<Buffer<int, 3>, Buffer<int, 3>> << '\n';
std::cout << sizeof(small.data) / sizeof(int) << '\n';
}
Example explained
Line 1Buffer<int, 3> and Buffer<int, 4> are two separately generated classes, so is_same_v reports false.
Line 2N is a compile-time constant inside the template, which is why size() can be constexpr and T data[N] is a real fixed array.
Line 3small{{1, 2, 3}} is ordinary aggregate initialisation: the instantiated class has no constructor, just the array member.
Dependent member types need typename
Shows why a type pulled out of a template parameter has to be announced as a type.
<iostream>
<type_traits>
<vector>
template <typename Container>
class Summary {
public:
using value_type = typename Container::value_type;
explicit Summary(const Container& c) : c_(c) {}
value_type total() const {
value_type sum{};
for (const value_type& v : c_) sum += v;
return sum;
}
private:
const Container& c_;
};
int main() {
std::vector<int> v{2, 3, 5, 7};
Summary<std::vector<int>> s(v);
std::cout << s.total() << '\n';
std::cout << std::boolalpha
<< std::is_same_v<Summary<std::vector<int>>::value_type, int> << '\n';
}
Example explained
Line 1typename Container::value_type is required because, while parsing the template, the compiler cannot know that value_type will turn out to name a type.
Line 2Naming Summary<std::vector<int>> is what instantiates the class and resolves the alias to int.
Line 3total() is instantiated at its call site, so sum += v is only checked against the element type at that moment.
Out-of-class members and explicit instantiation
Shows the syntax for defining a member outside the class and for forcing a whole specialisation to be generated.
<iostream>
template <typename T>
class Ring {
public:
explicit Ring(T v) : v_(v) {}
T twice() const; // declared here, defined below
private:
T v_;
};
template <typename T>
T Ring<T>::twice() const { return v_ + v_; }
template class Ring<int>; // explicit instantiation definition
int main() {
Ring<int> r(7);
std::cout << r.twice() << '\n';
}
Example explained
Line 1The out-of-class definition repeats template <typename T> and qualifies the name as Ring<T>::twice, because Ring on its own is a template name, not a type.
Line 2template class Ring<int>; generates every member of Ring<int> in this translation unit, whether or not anything calls them.
Line 3Since all members are generated, all of them must be valid for int: explicit instantiation gives up the usual per-member laziness.
Important notes
static int instances; inside the template only declares the member. Without the out-of-class template <typename T> int Box<T>::instances = 0; (or static inline int instances = 0; in C++17) the program fails to link.
From C++17 the arguments can be deduced from a constructor call, so Box b(21); means Box<int>. That deduction only applies to such declarations: data members, function parameters and declarations with no initialiser still need Box<int> written out.
Common mistakes
Splitting the template like an ordinary class, with the class in box.hpp and the member bodies in box.cpp: box.cpp compiles, but every other file fails to link with 'undefined reference to Box<int>::get()', because no translation unit saw the pattern where it was needed.
Writing the out-of-class definition as template <typename T> const T& Box::get() const, without <T> on Box; the compiler rejects it, since outside the class body Box names a template and only Box<T> names the class being defined.
Passing a Stack<Derived> where a Stack<Base> is expected; the two instantiations are unrelated types, so the call simply does not compile unless you add a converting constructor template yourself.
Try it yourself
Change, predict, then run
Rewrite Box as Pair<A, B> holding one value of each type, and give it a swapped() member returning Pair<B, A>. Instantiate Pair<int, std::string>, print both halves of the swapped result, and note that one line of code caused the compiler to generate two distinct classes.
Open the C++ workspaceCheck your understanding
A class template Wrap<T> has a member void shout() const { std::cout << v.size(); } where v is of type T. The line Wrap<int> w{5}; compiles and runs without error even though int has no size(). Why?
- The compiler discards members that are invalid for the given template argument.
- The error is deferred to link time, and disappears because shout is never linked.
- A member function body is instantiated only when that member is used, so shout is never compiled for T = int.
- int is given an implicit size() member when it is used as a template argument.
Show answer
Instantiating Wrap<int> instantiates the member declarations, which is all that is needed to know the class's layout and signatures; each body is instantiated at its first use, so nothing ever checks v.size(). The 'discards invalid members' option is tempting but wrong: Wrap<int>::shout still exists, and adding w.shout(); produces a compile error at that line, not a silently missing function.