Module 07: C++ Templates
Key Concepts:
- Function templates
- Class templates
- Template instantiation
- Template specialization
Why Templates? (Historical Context)
Section titled “Why Templates? (Historical Context)”Before C++ templates, C programmers used parametric macros to write generic code:
#define MAX(x, y) ((x) > (y) ? (x) : (y))
int result = MAX(3, 5); // Worksdouble d = MAX(3.14, 2.71); // WorksBut macros have serious problems:
- No type safety:
MAX("hello", 42)compiles but is nonsense - Text substitution bugs:
MAX(i++, j)incrementsitwice ifi > j - No scope: Macros pollute the global namespace
- No debugging: Errors point to expanded code, not the macro
Templates solve all these problems. The compiler:
- Checks types at compile time
- Generates real functions (no text substitution)
- Respects scope rules
- Provides (albeit verbose) type-checked error messages
The “Code with Holes” Mental Model
Section titled “The “Code with Holes” Mental Model”Think of templates as code patterns with holes (placeholders) that the compiler fills in:
template <typename T> // "T" is the holeT max(T a, T b) { // Pattern with holes return (a > b) ? a : b;}
max(3, 5); // Compiler fills hole with "int"max(3.14, 2.71); // Compiler fills hole with "double"When you use a template, the compiler instantiates it: it takes your pattern, fills in the holes with actual types, and generates real code. This is compile-time code generation, not runtime polymorphism.
Understanding Template Syntax
Section titled “Understanding Template Syntax”This module introduces templates. Here’s the new syntax explained:
The template <> Keyword
Section titled “The template <> Keyword”template <typename T>T max(T a, T b) { return (a > b) ? a : b; }template <>declares that what follows is a templatetypename T(orclass T) declares a type parameter calledTTcan be any type:int,double,std::string, your own class, etc.- When you use the template, the compiler generates code for the specific type
typenameandclassare interchangeable in C++98 (both mean the same thing)
The <> Angle Brackets (Template Parameters)
Section titled “The <> Angle Brackets (Template Parameters)”max<int>(3, 5); // Explicitly use intmax(3.14, 2.71); // Compiler deduces doubleArray<int> intArray; // Class template instantiation<>enclose template arguments- For functions, you can let the compiler deduce the type from arguments
- For classes, you must explicitly specify the type
- Read
Array<int>as “Array of int”
The :: Scope with Templates
Section titled “The :: Scope with Templates”std::vector<int>::iterator it;::with templates accesses nested typesvector<int>::iteratormeans “the iterator type inside vector-of-int”- Template classes can have their own nested types (types that depend on the template parameter)
The typename Keyword (Disambiguator)
Section titled “The typename Keyword (Disambiguator)”typename std::vector<T>::iterator it; // Tell compiler this is a typetypenamebeforeClassName::NestedTypetells the compiler “this is a type, not a value”- Inside templates, the compiler doesn’t know if
vector<T>::iteratoris a type or static member typenameremoves this ambiguity - it says “trust me, this is a type name”- Required when accessing nested types of template-dependent classes
Multiple Template Parameters
Section titled “Multiple Template Parameters”template <typename T, typename U>T convert(U value) { return static_cast<T>(value); }- You can have multiple template parameters separated by commas
Tis the return type,Uis the input type- Usage:
convert<int>(3.14)explicitly specifies T, compiler deduces U
Default Template Parameters
Section titled “Default Template Parameters”template <typename T = int>class Stack { };
Stack<> intStack; // Uses default: intStack<double> dblStack; // Explicit: double= intprovides a default type parameter- If not specified, the default is used
- Use
<>when you want the default (not justStack)
1. Why Templates?
Section titled “1. Why Templates?”The Problem: Code Duplication
Section titled “The Problem: Code Duplication”// Without templates - must write for each typeint maxInt(int a, int b) { return (a > b) ? a : b; }double maxDouble(double a, double b) { return (a > b) ? a : b; }std::string maxString(std::string a, std::string b) { return (a > b) ? a : b; }The Solution: Templates
Section titled “The Solution: Templates”// With templates - one definition, works for all typestemplate <typename T>T max(T a, T b) { return (a > b) ? a : b;}
// Usagemax(3, 5); // Instantiates max<int>max(3.14, 2.71); // Instantiates max<double>max(str1, str2); // Instantiates max<std::string>2. Function Templates
Section titled “2. Function Templates”Basic Syntax
Section titled “Basic Syntax”template <typename T>T functionName(T param1, T param2) { // T can be used like any type return param1 + param2;}Multiple Type Parameters
Section titled “Multiple Type Parameters”template <typename T, typename U>T convert(U value) { return static_cast<T>(value);}
// Usageint i = convert<int>(3.14); // Explicit T, deduced UTemplate vs typename
Section titled “Template vs typename”template <typename T> // Modern styletemplate <class T> // Also valid, means the same thing3. Module 07 Exercise 00: swap, min, max
Section titled “3. Module 07 Exercise 00: swap, min, max”// swap: Exchange values of two variablestemplate <typename T>void swap(T& a, T& b) { T temp = a; a = b; b = temp;}
// min: Return smaller value (return second if equal)template <typename T>T const& min(T const& a, T const& b) { return (a < b) ? a : b;}
// max: Return larger value (return second if equal)template <typename T>T const& max(T const& a, T const& b) { return (a > b) ? a : b;}Test Code
Section titled “Test Code”int main() { int a = 2; int b = 3;
::swap(a, b); std::cout << "a = " << a << ", b = " << b << std::endl; std::cout << "min(a, b) = " << ::min(a, b) << std::endl; std::cout << "max(a, b) = " << ::max(a, b) << std::endl;
std::string c = "chaine1"; std::string d = "chaine2";
::swap(c, d); std::cout << "c = " << c << ", d = " << d << std::endl; std::cout << "min(c, d) = " << ::min(c, d) << std::endl; std::cout << "max(c, d) = " << ::max(c, d) << std::endl;
return 0;}4. Module 07 Exercise 01: iter
Section titled “4. Module 07 Exercise 01: iter”Function Pointers as Template Parameters
Section titled “Function Pointers as Template Parameters”// iter: Apply function to each element of array// F can be a function pointer OR a functor typetemplate <typename T, typename F>void iter(T* array, size_t length, F func) { for (size_t i = 0; i < length; i++) { func(array[i]); }}Explicit Function Pointer Version
Section titled “Explicit Function Pointer Version”// More explicit: F is specifically a function pointertemplate <typename T>void iter(T* array, size_t length, void (*func)(T&)) { for (size_t i = 0; i < length; i++) { func(array[i]); }}Template Overloading: const vs non-const
Section titled “Template Overloading: const vs non-const”The same template can be overloaded for const and non-const operations:
// Non-const version - can modify elementstemplate <typename T>void iter(T* array, size_t length, void (*func)(T&)) { for (size_t i = 0; i < length; i++) { func(array[i]); }}
// Const version - read-only operationstemplate <typename T>void iter(T* array, size_t length, void (*func)(T const&)) { for (size_t i = 0; i < length; i++) { func(array[i]); }}Why Both Versions?
Section titled “Why Both Versions?”// Function that modifies (needs non-const reference)template <typename T>void increment(T& elem) { elem++;}
// Function that only reads (uses const reference)template <typename T>void print(T const& elem) { std::cout << elem << std::endl;}
int arr[] = {1, 2, 3};
// Calls non-const versioniter(arr, 3, increment<int>); // arr is now {2, 3, 4}
// Calls const versioniter(arr, 3, print<int>); // Prints elementsFunction Pointer Syntax with Templates
Section titled “Function Pointer Syntax with Templates”// When calling with a template function, you must instantiate it:iter(arr, 3, print<int>); // Explicit template argumentiter(arr, 3, &print<int>); // & is optional
// With non-template functions:void printInt(int const& x) { std::cout << x; }iter(arr, 3, printInt); // No <> neededTest Code
Section titled “Test Code”template <typename T>void print(T const& elem) { std::cout << elem << std::endl;}
template <typename T>void increment(T& elem) { elem++;}
int main() { int arr[] = {1, 2, 3, 4, 5};
std::cout << "Original:" << std::endl; iter(arr, 5, print<int>);
iter(arr, 5, increment<int>);
std::cout << "After increment:" << std::endl; iter(arr, 5, print<int>);
return 0;}5. Class Templates
Section titled “5. Class Templates”Basic Syntax
Section titled “Basic Syntax”template <typename T>class Container {private: T* _data; size_t _size;
public: Container(size_t size); Container(const Container& other); Container& operator=(const Container& other); ~Container();
T& operator[](size_t index); const T& operator[](size_t index) const; size_t size() const;};Implementation
Section titled “Implementation”// For class templates, implementation MUST be in header// Or in a .tpp file included by the header
template <typename T>Container<T>::Container(size_t size) : _size(size) { _data = new T[size](); // () for value initialization}
template <typename T>Container<T>::~Container() { delete[] _data;}
template <typename T>T& Container<T>::operator[](size_t index) { if (index >= _size) throw std::out_of_range("Index out of bounds"); return _data[index];}6. Module 07 Exercise 02: Array
Section titled “6. Module 07 Exercise 02: Array”template <typename T>class Array {private: T* _array; unsigned int _size;
public: // Default constructor - empty array Array() : _array(NULL), _size(0) {}
// Size constructor - array of n elements Array(unsigned int n) : _array(new T[n]()), _size(n) {}
// Copy constructor - deep copy Array(const Array& other) : _array(NULL), _size(0) { *this = other; }
// Assignment operator - deep copy Array& operator=(const Array& other) { if (this != &other) { delete[] _array; _size = other._size; _array = new T[_size]; for (unsigned int i = 0; i < _size; i++) _array[i] = other._array[i]; } return *this; }
// Destructor ~Array() { delete[] _array; }
// Element access with bounds checking T& operator[](unsigned int index) { if (index >= _size) throw std::out_of_range("Index out of bounds"); return _array[index]; }
const T& operator[](unsigned int index) const { if (index >= _size) throw std::out_of_range("Index out of bounds"); return _array[index]; }
// Size getter unsigned int size() const { return _size; }};std::out_of_range Exception
Section titled “std::out_of_range Exception”#include <stdexcept> // for std::out_of_range
// out_of_range is used for index/bounds errorsT& operator[](unsigned int index) { if (index >= _size) throw std::out_of_range("Index out of bounds"); return _array[index];}
// Usage:try { Array<int> arr(5); arr[10] = 42; // Throws out_of_range}catch (std::out_of_range& e) { std::cout << "Exception: " << e.what() << std::endl;}catch (std::exception& e) { // Catches any standard exception std::cout << "Error: " << e.what() << std::endl;}Exception Hierarchy for Reference
Section titled “Exception Hierarchy for Reference”std::exception├── std::logic_error│ ├── std::out_of_range <- Use for index errors│ ├── std::invalid_argument <- Use for bad function arguments│ └── std::length_error <- Use for size limit errors└── std::runtime_error <- Use for general runtime errorsTest Code
Section titled “Test Code”int main() { // Test default constructor Array<int> empty; std::cout << "Empty size: " << empty.size() << std::endl;
// Test size constructor Array<int> arr(5); std::cout << "Size: " << arr.size() << std::endl;
// Test element access for (unsigned int i = 0; i < arr.size(); i++) arr[i] = i * 2;
// Test copy Array<int> copy(arr); arr[0] = 100; std::cout << "arr[0]: " << arr[0] << std::endl; std::cout << "copy[0]: " << copy[0] << std::endl; // Should be 0
// Test bounds checking try { arr[100] = 42; } catch (std::exception& e) { std::cout << "Exception: " << e.what() << std::endl; }
return 0;}7. Template Instantiation
Section titled “7. Template Instantiation”Implicit Instantiation
Section titled “Implicit Instantiation”// Compiler generates code when template is usedArray<int> intArray; // Generates Array<int>Array<double> dblArray; // Generates Array<double>Explicit Instantiation
Section titled “Explicit Instantiation”// Force generation of specific types (rarely needed)template class Array<int>;template void swap<double>(double&, double&);8. Where to Put Template Code
Section titled “8. Where to Put Template Code”Option 1: All in Header (.hpp)
Section titled “Option 1: All in Header (.hpp)”template <typename T>class Array { // Declaration AND implementation here};Option 2: Separate .tpp File
Section titled “Option 2: Separate .tpp File”template <typename T>class Array { // Declaration only};
#include "Array.tpp" // Include implementation at end
// Array.tpptemplate <typename T>Array<T>::Array() { /* ... */ }The .tpp naming convention: Using .tpp (template implementation) instead of .cpp clearly identifies template implementation files in your codebase. When you see a .tpp file, you immediately know it contains template definitions that will be included in a header.
Why Not .cpp?
Section titled “Why Not .cpp?”Templates need to be visible at instantiation point. If implementation is in .cpp, the compiler can’t generate the code.
9. Template Specialization
Section titled “9. Template Specialization”Sometimes you need different behavior for specific types. Template specialization lets you provide custom implementations for particular template arguments.
Complete Specialization (Functions and Classes)
Section titled “Complete Specialization (Functions and Classes)”// Generic templatetemplate <typename T>void print(T value) { std::cout << value << std::endl;}
// Complete specialization for booltemplate <>void print<bool>(bool value) { std::cout << (value ? "true" : "false") << std::endl;}
// Usageprint(42); // Uses generic: prints "42"print(3.14); // Uses generic: prints "3.14"print(true); // Uses specialization: prints "true"Class Template Specialization
Section titled “Class Template Specialization”// Generic templatetemplate <typename T>class Storage { T _data;public: void store(T val) { _data = val; } T retrieve() { return _data; }};
// Complete specialization for char*template <>class Storage<char*> { std::string _data; // Store as string for safetypublic: void store(char* val) { _data = val; } const char* retrieve() { return _data.c_str(); }};Partial Specialization (Classes Only)
Section titled “Partial Specialization (Classes Only)”Partial specialization provides a template for a subset of types (e.g., all pointers):
// Generic templatetemplate <typename T>class Container {public: void info() { std::cout << "Generic container" << std::endl; }};
// Partial specialization for all pointer typestemplate <typename T>class Container<T*> {public: void info() { std::cout << "Pointer container" << std::endl; }};
// UsageContainer<int> c1; // Uses genericContainer<int*> c2; // Uses pointer specializationContainer<double*> c3; // Uses pointer specializationNote: Function templates cannot be partially specialized. If you need partial specialization behavior for functions, use overloading or a helper class template.
When to Use Specialization
Section titled “When to Use Specialization”- Optimization: Provide faster implementations for specific types
- Special behavior: Handle types that don’t work with the generic implementation
- Type traits: Build compile-time type information (like
is_pointerabove)
10. Debugging Template Errors
Section titled “10. Debugging Template Errors”Template error messages are notoriously difficult to read. A small mistake can generate pages of errors. This is a known challenge with templates, not a personal failure.
Tips for Debugging
Section titled “Tips for Debugging”-
Read from the bottom up: The actual error is often near the end; earlier messages show instantiation context
-
Look for “instantiated from”: This tells you WHERE the template was used, helping you trace the problem
-
Test with concrete types first: If your template doesn’t work, try writing the code for a specific type (like
int) first -
Check required operations: If your template uses
<, the type must supportoperator<
// This template requires operator< to exist for type Ttemplate <typename T>T min(T a, T b) { return (a < b) ? a : b;}
// This will fail with a confusing error:struct NoCompare { int x; };NoCompare a, b;min(a, b); // Error: no operator< for NoCompare- Simplify: Comment out parts of your template to isolate the problem
11. Common Template Patterns
Section titled “11. Common Template Patterns”Type Traits (C++98 style)
Section titled “Type Traits (C++98 style)”template <typename T>struct is_pointer { static const bool value = false;};
template <typename T>struct is_pointer<T*> { static const bool value = true;};Default Template Parameters
Section titled “Default Template Parameters”template <typename T = int>class Stack { // Default to int if no type specified};
Stack<> intStack; // Uses default: intStack<double> dblStack; // Explicit: doubleQuick Reference
Section titled “Quick Reference”Function Template
Section titled “Function Template”template <typename T>T functionName(T param) { /* ... */ }Class Template
Section titled “Class Template”template <typename T>class ClassName { // Members using T};
// Outside class definitiontemplate <typename T>ClassName<T>::methodName() { /* ... */ }Key Rules
Section titled “Key Rules”- Templates must be in headers (or included .tpp)
- Use
typenameorclass(interchangeable) - Types must support operations used in template
- Deep copy for pointer members in class templates