<< Home

Chapter 13: Templates (The Generics Engine)

Templates let you write algorithms and types that adapt to many concrete forms at compile time. They power the standard library, enable zero cost abstraction, and connect with type deduction, overload resolution, and instantiation rules. This chapter builds the essential mental model, then walks through declarations, deduction, non type parameters, specialization, and name lookup in dependent contexts.

Function and class templates

A function template is a pattern for generating functions; a class template is a pattern for generating classes. Instantiation happens when you use the template with specific arguments, and the compiler produces concrete entities for those arguments. You write one definition; the compiler synthesizes many versions as needed.

Function templates

Function templates parameterize types or values that appear in the function signature or body. You can let the compiler deduce template parameters from call arguments, or you can provide them explicitly when needed.

template <class T>
T max_of(T a, T b) { return b < a ? a : b; }

int a = max_of(3, 7);  // T = int
std::string s = max_of(std::string("a"), std::string("z"));  // T = std::string

Class templates

Class templates define families of types that vary by parameters. You can create type aliases for common specializations to reduce noise in client code.

template <class T, class Alloc = std::allocator<T>>
class SimpleVec {
  T* p{};
  std::size_t n{};
  Alloc alloc;
public:
  explicit SimpleVec(std::size_t count) : p(alloc.allocate(count)), n(count) {}
  /* … minimal vector-like operations … */
};

using Bytes = SimpleVec<std::byte>;
πŸ’‘ Give template parameters clear names such as T, Key, Value, or Traits; this improves readability and communicates intent.

Template argument deduction

When you call a function template without specifying parameters, the compiler deduces template arguments from the call. Deduction follows rules that compare the function parameter types against the argument expressions while respecting references, cv qualifiers, and value category.

Type deduction basics

Type deduction treats T&, const T&, and T differently. Forwarding references preserve lvalue or rvalue status; plain T by value strips references and cv qualifiers from the argument. Understanding this keeps copies controlled and avoids surprises.

template <class T>  // T is decayed; arg is copied or moved
void by_value(T x);

template <class T>  // T is deduced without adding const
void by_ref(T& x);

template <class T>  // forwarding reference; preserves value category
void forward_ref(T&& x);

int i = 0;
const int ci = 0;
by_value(i);      // T = int
by_ref(i);        // T = int
by_ref(ci);       // error; cannot bind non-const lvalue ref to const
forward_ref(i);   // T = int&; param type becomes int&& &  β†’  int&
forward_ref(42);  // T = int;   param type becomes int&&

Class template argument deduction and guides

Many class templates support CTAD which lets you write the constructor arguments and let the compiler deduce template parameters. Deduction guides are rules that direct CTAD when constructor signatures are not enough or are ambiguous.

std::pair p{42, 3.14};   // deduces std::pair<int,double>
std::vector v{1, 2, 3};  // deduces std::vector<int>

template <class T, class Alloc>
class Box { public: Box(T, Alloc); };

Box b{std::string{"x"}, std::allocator<char>{}};  // may need a guide if not obvious
⚠️ CTAD never performs narrowing conversions during deduction; add explicit constructors or deduction guides when element types must be fixed precisely.

Non type template parameters

Templates can also take constant values known at compile time. These are non type template parameters. They control sizes, policies, and behavior without runtime overhead. Since C++20, many literal types are allowed including pointers, references, and even some class types with structural semantics.

Using integral and pointer non type parameters

Classic examples include fixed size arrays and lookup tables. Pointers and references as parameters encode object locations or function links known at compile time; use carefully with linkage and odr rules.

template <std::size_t N>
struct Fixed {
  std::array<int, N> data{};
};

template <auto* Fn>
struct Callback {
  void call() const { (*Fn)(); }
};

void hello();
using Hello = Callback<&hello>;

Structural types and auto non type parameters

The auto placeholder permits concise non type parameters. Structural types such as std::integral_constant like values or user types that satisfy structural rules can also serve. This improves expressiveness while keeping strong compile time guarantees.

template <auto V>
constexpr auto value_v = V;

static_assert(value_v<42> == 42);
πŸ’‘ Prefer auto non type parameters when the exact type is obvious from usage; it reduces repetition and keeps signatures tidy.

Specialization and partial specialization

Specialization lets you customize a template for particular arguments. A full specialization fixes all parameters. A partial specialization matches a subset of patterns and participates in ordering rules. Function templates cannot be partially specialized; use overloading instead.

Full specialization for a specific case

Provide a full specialization when a specific set of parameters requires different representation or behavior. Place the specialization in the same namespace as the primary template to follow lookup rules.

template <class T>
struct Hash;

template <>
struct Hash<std::string> {
  std::size_t operator()(const std::string& s) const noexcept { /* … */ return 0; }
};

Partial specialization for families of cases

Partial specializations select among families by matching patterns. The most specialized viable match wins. Use these for traits and type transformations where structure matters more than values.

template <class T>
struct RemovePtr { using type = T; };

template <class T>
struct RemovePtr<T*> { using type = T; };

template <class T>
using RemovePtrT = typename RemovePtr<T>::type;
⚠️ Prefer specialization for class templates and overloads for function templates; attempting to partially specialize a function template is ill formed.

ADL and dependent names

ADL (Argument Dependent Lookup) searches the namespaces associated with the types of function call arguments. Dependent names are identifiers whose meaning depends on template parameters. These features interact strongly with templates and influence which functions are found and which names require annotation.

Making customization discoverable with ADL

Place free functions that customize algorithms in the same namespace as the types they extend. Calls that rely on ADL should use unqualified names so that lookup can find the right overload during instantiation.

namespace img {
  struct Pixel { int r, g, b; };
  void swap(Pixel& a, Pixel& b);  // found by ADL
}

template <class T>
void algo(T& a, T& b) {
  using std::swap;
  swap(a, b);  // finds img::swap via ADL or falls back to std::swap
}

Qualifying dependent types and templates

When a name depends on a template parameter, you may need keywords to disambiguate parsing. Use typename before dependent type names and use template before a dependent template when calling a member template. These markers tell the compiler to treat the names as types or templates during parsing.

template <class C>
void f(C& c) {
  using Iter = typename C::iterator;  // dependent type; needs typename
  Iter it = c.begin();
  c.template emplace_back(1, 2, 3);   // dependent template; needs template
}
πŸ’‘ If a name depends on a template parameter and denotes a type, add typename; if it denotes a template and is followed by <…>, add template. This keeps parsing unambiguous and your intent clear.