C++ · lesson 10 of 24
Beginner
6Intermediate
11- 5 Operators and Operator Overloading in C++
- 8 Classes and Object-Oriented Programming in C++
- 9 Inheritance and Polymorphism in C++
- 11 STL Containers in C++
- 12 STL Algorithms in C++
- 13 Smart Pointers in C++
- 15 Lambdas and Functional Programming in C++
- 18 Error Handling in C++
- 19 File I/O in C++
- 23 C++ Interview Preparation: Top 35 Questions
- 24 Static Members in C++
Advanced
7Templates in C++
C++ templates with real compiler output: function and class templates, specialization, type traits, SFINAE vs concept errors and compile-time recursion.
By SkillByExample · Updated
Templates let you write code once and have the compiler generate a concrete version for every type you use. It all happens at compile time — std::vector<int> and std::vector<std::string> are separate classes with no runtime genericity cost.
Function Templates
A function template takes type parameters that the compiler deduces from the arguments. GCC’s __PRETTY_FUNCTION__ shows which instantiation actually ran.
#include <iostream>
#include <string>
template <typename T>
T larger(T a, T b) {
std::cout << "[" << __PRETTY_FUNCTION__ << "] ";
return (a > b) ? a : b; // works for any T with operator>
}
int main() {
std::cout << larger(3, 7) << "\n";
std::cout << larger(3.14, 2.72) << "\n";
std::cout << larger('a', 'z') << "\n";
std::cout << larger<double>(3, 7.5) << "\n"; // explicit T: 3 converts
std::cout << larger(std::string("pear"), std::string("apple")) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o max max.cpp
$ ./max
[T larger(T, T) [with T = int]] 7
[T larger(T, T) [with T = double]] 3.14
[T larger(T, T) [with T = char]] z
[T larger(T, T) [with T = double]] 7.5
[T larger(T, T) [with T = std::__cxx11::basic_string<char>]] pear
Four distinct functions were generated: int, double, char and std::string (spelled std::__cxx11::basic_string<char> inside libstdc++). The larger<double>(3, 7.5) call names T explicitly, because deduction from 3 and 7.5 would conflict — int versus double. Strings compare lexicographically, so “pear” wins. The function is named larger rather than max to avoid colliding with std::max through argument-dependent lookup on std::string.
Class Templates
A class template parameterizes a whole class. Stack<int> and Stack<std::string> are two unrelated types generated from one definition.
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
template <typename T>
class Stack {
std::vector<T> data_;
public:
void push(T value) { data_.push_back(std::move(value)); }
T pop() {
if (data_.empty()) throw std::underflow_error("pop on empty Stack");
T top = std::move(data_.back());
data_.pop_back();
return top;
}
bool empty() const { return data_.empty(); }
};
int main() {
Stack<int> ints;
for (int i : {1, 2, 3}) ints.push(i);
while (!ints.empty()) std::cout << ints.pop() << " ";
std::cout << "\n";
Stack<std::string> words; // a separate class, generated on demand
words.push("template");
std::cout << words.pop() << "\n";
try { words.pop(); } catch (const std::underflow_error& e) { std::cout << e.what() << "\n"; }
}
$ g++ -std=c++20 -Wall -Wextra -o stack stack.cpp
$ ./stack
3 2 1
template
pop on empty Stack
The stack is last-in, first-out, so 1, 2, 3 come back as 3 2 1. push takes T by value and moves it into the vector — one signature that copies lvalues and moves rvalues. Member functions of a class template are only instantiated when used, so a Stack<T> whose T can’t be moved still compiles as long as nobody calls push or pop, which both move.
Non-Type Template Parameters
Template parameters can be compile-time values. This is how std::array<T, N> carries its size in the type.
#include <array>
#include <iostream>
#include <numeric>
template <typename T, std::size_t N>
T sum(const std::array<T, N>& arr) { // N deduced from the type
return std::accumulate(arr.begin(), arr.end(), T{});
}
template <int Capacity>
class RingBuffer {
int data_[Capacity]{}; // size fixed at compile time
int next_ = 0;
public:
void add(int v) { data_[next_] = v; next_ = (next_ + 1) % Capacity; }
int at(int i) const { return data_[i]; }
static constexpr int capacity() { return Capacity; }
};
int main() {
std::array<int, 5> a{1, 2, 3, 4, 5};
std::array<double, 2> b{0.5, 0.25};
std::cout << sum(a) << " " << sum(b) << "\n";
RingBuffer<3> rb;
for (int v = 1; v <= 4; ++v) rb.add(v); // 4 overwrites 1
std::cout << rb.at(0) << rb.at(1) << rb.at(2) << ", capacity "
<< RingBuffer<3>::capacity() << ", sizeof " << sizeof(rb) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o nttp nttp.cpp
$ ./nttp
15 0.75
423, capacity 3, sizeof 16
sum deduces both T and N from std::array<int, 5>, so there is no size argument to pass or get wrong. RingBuffer<3> holds a plain int[3]: after four adds, 4 has overwritten slot 0, and sizeof is 16 bytes (12 of data + 4 for next_) with no heap allocation. RingBuffer<4> would be a different type.
Template Specialization
When the generic version is wrong for a specific type, specialize. Functions allow only full specialization; classes also allow partial specialization for a family of types.
#include <cstring>
#include <iostream>
#include <string>
template <typename T>
bool equal(T a, T b) { return a == b; }
template <> // full specialization
bool equal<const char*>(const char* a, const char* b) {
return std::strcmp(a, b) == 0; // compare content, not addresses
}
template <typename T> struct TypeName { static std::string get() { return "something"; } };
template <> struct TypeName<int> { static std::string get() { return "int"; } };
template <typename T> struct TypeName<T*> { // partial: any pointer
static std::string get() { return "pointer to " + TypeName<T>::get(); }
};
int main() {
char buf1[] = "hello", buf2[] = "hello"; // two different arrays
const char* p1 = buf1;
const char* p2 = buf2;
std::cout << std::boolalpha;
std::cout << "equal(p1, p2): " << equal(p1, p2) << "\n";
std::cout << "p1 == p2: " << (p1 == p2) << "\n";
std::cout << "equal(2, 2): " << equal(2, 2) << "\n";
std::cout << TypeName<int**>::get() << ", " << TypeName<double*>::get() << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o specialize specialize.cpp
$ ./specialize
equal(p1, p2): true
p1 == p2: false
equal(2, 2): true
pointer to pointer to int, pointer to something
buf1 and buf2 are separate arrays holding the same text: comparing the pointers says false, but the const char* specialization compares with strcmp and says true. TypeName<T*> is a partial specialization matching any pointer, and it recurses: int** peels one * at a time until it hits the full specialization for int. double has no specialization, so it falls back to the primary template’s “something”.
Try the same partial trick on a function and it fails:
template <typename T>
bool equal(T a, T b) { return a == b; }
template <typename T>
bool equal<T*>(T* a, T* b) { return *a == *b; } // partial specialization of a function
int main() {}
$ g++ -std=c++20 -Wall -Wextra -o partial_fail partial_fail.cpp 2>&1 | grep error
partial_fail.cpp:5:6: error: non-class, non-variable partial specialization 'equal<T*>' is not allowed
Use an overload — template <typename T> bool equal(T* a, T* b) — which is a separate template that overload resolution prefers for pointers.
Type Traits
<type_traits> answers questions about types at compile time. They are the building blocks for if constexpr, SFINAE and concepts.
#include <iostream>
#include <string>
#include <type_traits>
template <typename T>
void describe(const char* name) {
std::cout << std::boolalpha << name << ": integral " << std::is_integral_v<T>
<< ", pointer " << std::is_pointer_v<T> << ", const " << std::is_const_v<T>
<< ", const after remove_pointer "
<< std::is_const_v<std::remove_pointer_t<T>> << "\n";
}
int main() {
describe<int>("int");
describe<const int*>("const int*");
describe<int* const>("int* const");
describe<bool>("bool");
using D = std::decay_t<const std::string&>; // strips const and &
using C = std::common_type_t<int, long, float>;
std::cout << "decay_t<const string&> is string: " << std::is_same_v<D, std::string> << "\n";
std::cout << "common_type_t<int, long, float> is float: " << std::is_same_v<C, float> << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o traits traits.cpp
$ ./traits
int: integral true, pointer false, const false, const after remove_pointer false
const int*: integral false, pointer true, const false, const after remove_pointer true
int* const: integral false, pointer true, const true, const after remove_pointer false
bool: integral true, pointer false, const false, const after remove_pointer false
decay_t<const string&> is string: true
common_type_t<int, long, float> is float: true
The two pointer rows are the ones people get wrong. const int* is a (non-const) pointer to const int: the pointer itself isn’t const, the pointee is. int* const is the reverse — a const pointer to a mutable int. bool is an integral type. std::decay_t strips references and const the way passing by value does, and std::common_type_t<int, long, float> is float, the type the three would convert to in a ?: expression.
SFINAE with std::enable_if
SFINAE — substitution failure is not an error — lets a template remove itself from overload resolution when its signature can’t be formed.
#include <iostream>
#include <type_traits>
template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void print_bits(T value) {
for (int i = sizeof(T) * 8 - 1; i >= 0; --i) std::cout << ((value >> i) & 1);
std::cout << "\n";
}
int main() {
print_bits(42);
print_bits(static_cast<unsigned char>(5));
#ifdef BAD
print_bits(3.14); // no integral T: the template is discarded
#endif
}
$ g++ -std=c++20 -Wall -Wextra -o sfinae sfinae.cpp
$ ./sfinae
00000000000000000000000000101010
00000101
$ g++ -std=c++20 -Wall -Wextra -DBAD -o sfinae sfinae.cpp 2>&1 | grep -E 'error|note'
sfinae.cpp:14:15: error: no matching function for call to 'print_bits(double)'
sfinae.cpp:5:6: note: candidate: 'template<class T, class> void print_bits(T)'
sfinae.cpp:5:6: note: template argument deduction/substitution failed:
/usr/include/c++/13/type_traits:2610:11: error: no type named 'type' in 'struct std::enable_if<false, void>'
For int and unsigned char the default template argument std::enable_if_t<true> is void, and the function works. For double, enable_if<false> has no type member, substitution fails, and the template is silently discarded — leaving no candidate at all. The error message points into /usr/include/c++/13/type_traits, which is typical: SFINAE errors describe the machinery, not your intent.
C++20 Concepts
Concepts name a requirement and check it directly. Here’s the same print_bits with the standard std::integral concept:
#include <concepts>
#include <iostream>
void print_bits(std::integral auto value) {
for (int i = sizeof(value) * 8 - 1; i >= 0; --i) std::cout << ((value >> i) & 1);
std::cout << "\n";
}
int main() {
print_bits(42);
print_bits(3.14);
}
$ g++ -std=c++20 -Wall -Wextra -o concepts_fail concepts_fail.cpp 2>&1 | grep -E 'error|note'
concepts_fail.cpp:11:15: error: no matching function for call to 'print_bits(double)'
concepts_fail.cpp:4:6: note: candidate: 'template<class auto:16> requires integral<auto:16> void print_bits(auto:16)'
concepts_fail.cpp:4:6: note: template argument deduction/substitution failed:
concepts_fail.cpp:4:6: note: constraints not satisfied
/usr/include/c++/13/concepts:100:24: note: the expression 'is_integral_v<_Tp> [with _Tp = double]' evaluated to 'false'
Compare with the SFINAE error: GCC now says “constraints not satisfied” and shows the exact check that was false — is_integral_v<double>. Writing your own concepts:
#include <concepts>
#include <iostream>
#include <string>
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // a + b must compile and convert back to T
};
template <Addable T>
T sum(T a, T b) { return a + b; }
template <typename T>
requires std::totally_ordered<T>
T clamp(T v, T lo, T hi) { return v < lo ? lo : v > hi ? hi : v; }
auto add(std::integral auto a, std::integral auto b) { return a + b; } // abbreviated
int main() {
std::cout << sum(1, 2) << " " << sum(1.0, 2.5) << " "
<< sum(std::string("a"), std::string("b")) << "\n";
std::cout << std::boolalpha << "sum(true, true) = " << sum(true, true) << "\n";
std::cout << clamp(15, 0, 10) << " " << add(2, 40L) << "\n";
std::cout << "Addable<int*>: " << Addable<int*> << ", Addable<char>: "
<< Addable<char> << "\n";
}
$ g++ -std=c++20 -Wall -Wextra -o concepts concepts.cpp
$ ./concepts
3 3.5 ab
sum(true, true) = true
10 42
Addable<int*>: false, Addable<char>: true
A requires expression lists what must compile: here, a + b must exist and convert back to T. int, double and std::string all satisfy Addable; int* doesn’t, because adding two pointers is ill-formed. The surprise is bool: true + true is the int 2, which converts back to bool as true, so the concept is satisfied and sum(true, true) compiles. Concepts check syntax, not meaning — a concept is only as strict as its requirements. add(2, 40L) shows the abbreviated form: each std::integral auto is its own template parameter, so int and long mix freely.
Template Metaprogramming Basics
Before constexpr, compile-time computation was done with recursive template specializations. You’ll meet this style in older code.
#include <iostream>
template <int N>
struct Factorial { static constexpr long value = N * Factorial<N - 1>::value; };
template <>
struct Factorial<0> { static constexpr long value = 1; }; // base case
constexpr long factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); }
int main() {
static_assert(Factorial<5>::value == 120);
static_assert(factorial(5) == 120);
std::cout << Factorial<20>::value << " " << factorial(20) << "\n";
int n = 10;
std::cout << factorial(n) << "\n"; // the constexpr version also runs at runtime
// Factorial<n>::value would not compile: n is not a constant
}
$ g++ -std=c++20 -Wall -Wextra -o tmp tmp.cpp
$ ./tmp
2432902008176640000 2432902008176640000
3628800
Factorial<20> instantiates Factorial<19> down to the Factorial<0> specialization, and each value is computed by the compiler. The constexpr function produces the identical 2432902008176640000 (20! still fits in a 64-bit long) and can also run on a runtime value like n, which Factorial<n> cannot. Prefer constexpr functions for values; reserve template metaprogramming for computing types.
Practice
-
Add an overload
template <typename T> bool equal(T* a, T* b)tospecialize.cppthat compares pointees, and call it with twoint*pointing at equal values.Solution
template <typename T> bool equal(T* a, T* b) { return *a == *b; } int x = 5, y = 5; std::cout << equal(&x, &y) << "\n"; // trueequal(&x, &y)istrue. But checkequal(p1, p2)again withbuf2changed to"help!": it now printstruetoo. Explicit specializations don’t take part in overload resolution — the compiler first picks between the two primary templates, prefers the more specializedT*one for any pointer, and only then would look for specializations of the winner. Soconst char*now goes to the pointee overload, which compares just the first characters. Mixing specializations and overloads is fragile; for C-strings, add a plain non-templatebool equal(const char*, const char*), which beats both templates. -
Add
const T& peek() consttoStackand makepop()on an empty stack return an emptystd::optional<T>instead of throwing.Solution
std::optional<T> pop() { if (data_.empty()) return std::nullopt; T top = std::move(data_.back()); data_.pop_back(); return top; } const T& peek() const { return data_.back(); } // precondition: !empty()Callers then write
while (auto v = s.pop()) use(*v);. -
Tighten
Addablesoboolno longer satisfies it.Solution
template <typename T> concept Addable = !std::same_as<T, bool> && requires(T a, T b) { { a + b } -> std::convertible_to<T>; };sum(true, true)now fails with “constraints not satisfied”, andAddable<bool>printsfalse. -
Rewrite
print_bitswithif constexprinstead of a constraint, printing"not an integer"for other types.Solution
template <typename T> void print_bits(T value) { if constexpr (std::is_integral_v<T>) { for (int i = sizeof(T) * 8 - 1; i >= 0; --i) std::cout << ((value >> i) & 1); std::cout << "\n"; } else { std::cout << "not an integer\n"; } }The difference: now
print_bits(3.14)compiles and runs. A constraint rejects the call at compile time;if constexpraccepts it and picks behavior per type. -
Write a class template
Pair<T, U>with aswap()member returningPair<U, T>, and use class template argument deduction:Pair p{1, std::string("one")};.Solution
template <typename T, typename U> struct Pair { T first; U second; Pair<U, T> swap() const { return {second, first}; } }; template <typename T, typename U> Pair(T, U) -> Pair<T, U>; // deduction guide Pair p{1, std::string("one")}; auto q = p.swap(); // Pair<std::string, int>For an aggregate like this, C++20 deduces the arguments without the guide in GCC 13 as well; the explicit guide makes it work in C++17.