C++ Part I · Module 18 · Templates and Generic Programming
Class Templates
Every container in the standard library is a class parameterised by a type, so this is the shape the next two modules are built on. The syntax is a small extension of Chapter 18.2's, and the interesting part is what the result actually is — because the usual mental picture, of one class that can hold different things, is wrong in a way that matters. A class template is not a type. Supplying a type argument produces a type, and supplying a different one produces a completely separate type with its own size, its own members, and its own static data, related to the first only by having been generated from the same template. This chapter measures all of that: two instantiations reporting as different types, their statics holding independent values, their sizes following the parameter, and an assignment between them rejected by the compiler in four words. It also measures something that explains a great deal about how templates behave in practice: a member function containing outright nonsense for the supplied type compiles without complaint, as long as nobody calls it. And it covers the C++17 addition that makes class templates pleasant to use — deducing the type argument from a constructor call, which looks like the deduction of the previous chapter and follows different rules.
Intermediate17 min readC++Part IFundamentalsTemplates
Part I · Module 18 · Chapter 18.4 · Class Templates
1. The Syntax
template <typename T>
class Box {
public:
explicit Box(T v) : v_(std::move(v)) {}
const T& get() const { return v_; }
private:
T v_;
};The template <typename T> line is the same as Chapter 18.2's. What follows is an ordinary class definition in which T may be used anywhere a type may be used — as a member's type, a parameter, a return type, a base class.
Three details.
explicit because Box has a one-argument constructor, which Chapter 12.5 §1 established is a conversion unless you say otherwise.
std::move(v) because the parameter is a by-value T the constructor is finished with — Chapter 13.9 §4's first case.
And const T& get() rather than T get(), so a Box<std::string> does not copy on every read.
2. Using It
Box<int> bi(42);
Box<std::string> bs("text");The angle brackets are part of the type name. Box alone is not a type — it is a template — and Box<int> is the type.
3. Each Instantiation Is a Distinct Type
This is the point people get wrong, and it is measurable:
A. each instantiation is a DISTINCT type:
is_same<Box<int>, Box<double>> = 0
is_same<Box<int>, Box<int>> = 1Box<int> and Box<double> are not the same type, and they are not related — neither is a base of the other, neither converts to the other. They were generated from one template and that is the whole of their connection.
The consequence, measured:
Box<int> a(1);
Box<double> b(2.0);
a = b;error: no viable overloaded '='Four words. Box<int>::operator= takes a const Box<int>&, a Box<double> is not one, and there is no conversion. This surprises people who picture Box as one flexible class; it is immediately obvious once you picture it as a generator that has produced two unrelated classes.
A class template is not a type. It is a recipe for types, and each type it produces is as unrelated to the others as two classes you wrote by hand.
4. Independent Statics
The sharpest demonstration of §3:
template <typename T> class Box { /* ... */ static int instances; };
template <typename T> int Box<T>::instances = 0;B. and they have independent statics:
Box<int>::instances=5 Box<double>::instances=99One declaration, two separate variables. Chapter 11.8 established that a static member is one object shared by all instances of a class — and Box<int> and Box<double> are different classes, so each gets its own.
That is occasionally exactly what you want (a per-type counter, a per-type cache) and occasionally a surprise (a registry you expected to be global is per-instantiation).
Note the definition syntax: a static data member of a class template is itself a template and needs the template <typename T> prefix at its definition.
5. Sizes Follow the Parameter
C. sizes follow the parameter:
sizeof(Box<char>)=1 sizeof(Box<int>)=4 sizeof(Box<std::string>)=24Each instantiation is laid out independently, because T v_; is a real member of the generated class. There is no indirection, no type-erasure box, no common header — a Box<char> is one byte, exactly as a hand-written struct { char v_; } would be.
This is the structural reason for Chapter 18.1 §6's code-size measurement: separate types mean separate layouts and separate member functions, which is why eight instantiations produced 843 bytes against 180 for one.
6. Deducing the Type Argument (C++17)
Writing Box<int> when the argument is obviously an int is noise, and since C++17 it can be omitted:
Box b{42}; // no <int>D. C++17 class template argument deduction (CTAD):
Box b{42}; b.get()=42 is Box<int>? 1The type was deduced from the constructor argument. This is class template argument deduction, and it is worth being clear that it is a different mechanism from Chapter 18.3's.
| Function template deduction | CTAD | |
|---|---|---|
| deduces from | the call's arguments | the constructor's arguments |
| available since | always | C++17 |
| rules | Chapter 18.3's table | overload resolution over the constructors |
| can be guided | no | yes — deduction guides |
The practical differences: CTAD works only when the constructor arguments determine the type, so Box<std::vector<int>> b{}; still needs its brackets; and a library can supply deduction guides to steer it, which is how std::vector v{1,2,3} deduces std::vector<int> rather than something unhelpful.
Chapter 18.3's rules still apply inside the constructor's own deduction, which is why Box b{42} gives Box<int> and not Box<const int&>.
7. Unused Members Are Never Compiled
A measurement that explains a great deal:
template <typename T>
class Box {
public:
explicit Box(T v) : v_(v) {}
T get() const { return v_; }
void brokenUnlessCalled() { v_.thisDoesNotExist(); } // nonsense for T = int
private:
T v_;
};
Box<int> b(7);
b.get();compile exit: 0 errors: 0
Box<int> b(7); b.get() = 7
brokenUnlessCalled() was never called -- and never compiledbrokenUnlessCalled contains outright nonsense for T = int and the program compiled cleanly, because nobody called it. Instantiating Box<int> instantiates the class's declarations — enough to know its layout and its interface — and each member function's body only when it is used.
Two consequences, one convenient and one not.
It lets a class template support operations only some types can perform. A Box<T> can offer a print() that requires operator<< and still be usable with a type that has none, as long as that type's box is never printed. Much of the standard library relies on this.
And it means a class template can harbour code that has never been compiled for any type you use. A member nobody calls is a member nobody has checked, which is why template-heavy code needs tests that actually instantiate the combinations it claims to support.
Chapter 18.6 is the chapter on why this happens, including the part §7 glosses over: some errors in an uninstantiated template are caught anyway.
8. Member Functions Defined Outside the Class
For anything longer than a line, the out-of-class form:
template <typename T>
class Box {
public:
explicit Box(T v);
const T& get() const;
private:
T v_;
};
template <typename T>
Box<T>::Box(T v) : v_(std::move(v)) {}
template <typename T>
const T& Box<T>::get() const { return v_; }Two things to get right, both of which produce confusing errors when wrong.
Each definition repeats template <typename T>. It is a template in its own right.
And the class name is Box<T>, not Box, before the ::. After the :: — in the return type, the parameter list, the body — plain Box refers to Box<T> and either spelling works.
These definitions still go in the header. Chapter 18.7 measures what happens when they do not, and it is a link error rather than a compile error.
9. What Else Can Be a Parameter
A class template may take several parameters, mix kinds, and have defaults:
template <typename T, typename Allocator = std::allocator<T>> class Vector;
template <typename Key, typename Value> class Map;
template <typename T, std::size_t N> class Array; // a VALUE parameterThe last is a non-type template parameter and is Chapter 18.5's subject — it is the form that matters most in hardware-adjacent code, where a width or a depth is a compile-time constant.
Defaults work as you would expect and are how std::vector<int> means std::vector<int, std::allocator<int>>.
10. The Shape Every Container Has
Putting it together, the standard shape:
template <typename T>
class Stack {
public:
void push(const T& v) { data_.push_back(v); }
void push(T&& v) { data_.push_back(std::move(v)); }
T pop() { T v = std::move(data_.back()); data_.pop_back(); return v; }
bool empty() const { return data_.empty(); }
std::size_t size() const { return data_.size(); }
private:
std::vector<T> data_;
};Every decision there has appeared earlier: composition rather than inheritance (Chapter 16.7 §4 measured a forwarding call compiling to one identical instruction), an overload pair for lvalue and rvalue arguments (Chapter 13.8), std::move out of a member being consumed (Chapter 13.9 §4), and the Rule of Zero — no destructor, no copy or move operations, because std::vector<T> manages everything (Chapter 13.11 §1).
That last point is the one worth emphasising for class templates specifically. A template that holds its data in a standard container needs none of the Rule of Five, and it gets correct copy, move and destruction behaviour for every T automatically.
11. Measuring the Rule of Zero — and a Trait That Lies
§10 claimed Stack<T> gets correct copy, move and destruction behaviour for every T automatically. That is worth checking, because "for every T" is a strong claim.
A. a copyable T -- what did we get for free?
copy_constructible<Stack<std::string>> = 1
move_constructible<Stack<std::string>> = 1
destructible<Stack<std::string>> = 1
D. a copyable stack really copies:
a.size()=2 b.size()=2 (independent)
b.pop()=two then a.size()=2All three for free, and the copy is a real deep copy — popping from b left a at size 2. No special member was written.
And with a move-only element type, the template unchanged:
C. and it actually works:
after move: moved.size()=2 s.size()=0
popped *p=9 moved.size()=1Stack<std::unique_ptr<int>> moved correctly, leaving the source empty — again with nothing written. So the claim holds.
But one of my measurements contradicted its own label, and the correction is the useful part. I expected the trait to report that a move-only stack is not copyable. It reports the opposite:
is_copy_constructible<unique_ptr<int>> = 0 (the element: honestly false)
is_copy_constructible<vector<unique_ptr>> = 1 (the vector: declared, so true)And attempting the copy fails:
Stack<std::unique_ptr<int>> b = a;
error: call to implicitly-deleted copy constructor of 'std::unique_ptr<int>'
(30 diagnostic lines)is_copy_constructible_v said yes and the copy does not compile. The trait asks whether the declaration is viable, and std::vector's copy constructor is declared unconditionally — it only fails when its body is instantiated for a non-copyable element. So the trait is reporting on a declaration while the truth lives in a body nobody has compiled yet.
That is §7's finding seen from a different angle, and it is the second time this curriculum has caught the same trap: Chapter 13.10 §4 recorded is_move_constructible giving the opposite answer to the truth, and Chapter 13.10 §5 drew the method lesson from it. A type trait tells you what is declared. Instantiating tells you what works. Where it matters, the reliable instrument is to attempt the operation — in a test, or in a static_assert on the thing you actually care about.
12. What to Actually Do
Write template <typename T> and then an ordinary class.
Remember Box is not a type — Box<int> is.
Expect instantiations to be unrelated — no conversions, independent statics.
Use CTAD where the constructor determines the type — Box b{42}.
Repeat template <typename T> on out-of-class definitions, and qualify with Box<T>::.
Keep definitions in the header — Chapter 18.7.
Follow the Rule of Zero — hold data in a standard container and write none of the five.
And instantiate deliberately in tests, because an uncalled member has never been compiled.
13. Mistakes Worth Making Once
Treating Box as a type. It is a template; Box<int> is the type.
Expecting Box<int> and Box<double> to convert. Measured: no viable overloaded '='.
Expecting a static member to be shared across instantiations. Measured: 5 and 99.
Forgetting template <typename T> on an out-of-class member definition.
Writing Box::get instead of Box<T>::get before the ::.
Putting member definitions in a .cpp. A link error — Chapter 18.7.
Assuming a class template is fully compiled. Measured: a member with nonsense in it compiled, because it was never called.
Hand-writing the Rule of Five in a class template. It must be correct for every T, including ones that do not exist yet.
Expecting CTAD to work without constructor arguments. Box b{}; cannot deduce.
14. Interview Relevance
"What is a class template?" A recipe for types. The strong answer stresses that Box is not a type and each instantiation is a distinct, unrelated one.
"Are Box<int> and Box<double> related?" No — measured, assignment gives no viable overloaded '='. No base-derived relationship, no conversion.
"What happens to a static member?" One per instantiation — measured holding 5 and 99 independently.
"Is the whole class compiled when you instantiate it?" No — only the declarations plus the bodies of members you call. Measured: a member containing nonsense compiled because it was never called.
"Why does that matter?" It lets a template support operations only some types can perform, and it means an uncalled member has never been checked.
"What is CTAD?" C++17 deduction of a class template's arguments from the constructor call — a different mechanism from function template deduction, steerable with deduction guides.
"Which special members should a class template write?" Ideally none — the Rule of Zero, which matters more here because hand-written ones must be correct for every T.
15. What You Should Be Able to Say Now
template <typename T>followed by an ordinary class, withTusable anywhere a type is.Boxis not a type;Box<int>is — the angle brackets are part of the name.- Measured:
is_same<Box<int>, Box<double>>is 0 — distinct, unrelated types. - Measured:
Box<int> a; Box<double> b; a = b;givesno viable overloaded '='. - Measured: statics are independent —
Box<int>::instances=5,Box<double>::instances=99. - Measured: sizes follow the parameter — 1, 4 and 24 bytes for
char,intandstd::string. - That is the structural reason for Chapter 18.1 §6's 180 → 843 bytes across eight instantiations.
- Measured: CTAD deduced
Box<int>fromBox b{42}. - CTAD is a different mechanism from function template deduction — constructor-based, C++17, steerable by deduction guides.
- Measured: a member function containing nonsense for
T = intcompiled, because it was never called. - Only the declarations plus the called bodies are instantiated.
- That is a feature — operations only some types support — and a hazard: an uncalled member is unchecked.
- Out-of-class definitions repeat
template <typename T>and qualify withBox<T>::. - Definitions belong in the header — Chapter 18.7.
- Follow the Rule of Zero, because hand-written special members must be correct for every
T. - Instantiate deliberately in tests, or use Chapter 18.6 §8's explicit instantiation.
16. Exercises
Exercise 1 — write one. A Pair<A, B> holding two values of possibly different types, with accessors.
Exercise 2 — distinctness. Say what is_same<Pair<int,int>, Pair<int,double>> reports and what happens if you assign one to the other.
Exercise 3 — the static. Add an instance counter and say how many separate counters exist for Pair<int,int>, Pair<int,double> and Pair<double,int>.
Exercise 4 — sizes. Predict sizeof for Box<char>, Box<double> and Box<std::string>, and explain why there is no common header.
Exercise 5 — CTAD. Say which of Box b{42}, Box b{}, and Box<std::string> b{} compile, and why.
Exercise 6 — the unchecked member. Add a print() to Box requiring operator<<, instantiate Box<NoStreamType>, and say what happens — with and without calling print().
Exercise 7 — out of class. Move your accessors out of the class definition and get the two qualifications right.
Exercise 8 — the Rule of Zero. Write a Stack<T> needing none of the five, and say what would have to be correct if you wrote them by hand.
17. Summary
- A class template is a recipe for types, not a type —
Box<int>is the type. - Measured:
Box<int>andBox<double>are distinct and unrelated (is_same= 0), and assignment givesno viable overloaded '='. - Measured: statics are per-instantiation — 5 and 99.
- Measured: sizes follow the parameter — 1, 4, 24 — which is why code size grows per instantiation.
- Measured: CTAD deduced
Box<int>fromBox b{42}— a C++17 mechanism distinct from function template deduction. - Measured: an uncalled member function containing nonsense compiled — only declarations plus called bodies are instantiated.
- That is both a feature and a testing obligation: an uncalled member has never been checked.
- Out-of-class definitions repeat the template header and qualify with
Box<T>::, and belong in the header file. - Follow the Rule of Zero, because hand-written special members must be correct for every
Tanyone ever supplies.
The two template forms are now in place, and both have been parameterised on types. The frozen description of the next chapter makes a claim worth taking seriously for this audience: that parameterising on a value is the form that matters most in hardware-adjacent code. A register is eight or sixteen or thirty-two bits wide, a FIFO has a depth, a bus has a number of lanes — and all of those are known when the code is compiled, which means they can be template arguments rather than constructor arguments. The next chapter is that form, and the measurements are the reason it is worth preferring: the storage comes out exact rather than rounded to a pointer and a length, the value is usable in a constant expression including as an array bound, and each distinct width is a distinct type, so a thirty-two-bit field and an eight-bit field cannot be mixed up by accident.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
