Chapter 18

Templates & Tuples

Template functions, template classes, specialization, variadic templates, static_assert, generic programming, and std::tuple.

In this chapter

  1. What Are Templates?
  2. Template Functions
  3. Template Classes
  4. Template Specialization
  5. Template Static Members
  6. Variadic Templates
  7. static_assert
  8. typename vs class
  9. Generic Programming Constraints
  10. Copy/Assignment with Templates
  11. std::tuple
1

What Are Templates?

Templates allow you to define a behavior (function or class) that works with objects of varying types — without writing separate code for each type. Unlike macros, templates are type-safe: the compiler generates a concrete version for each type you use.

// General syntax
template <parameter list>
// function or class declaration

Template parameters can have defaults:

// T2 defaults to T1's type, T3 defaults to int
template <typename T1, typename T2 = T1, typename T3 = int>
bool TemplateFunction(T1 p1, T2 p2, T3 p3);

2

Template Functions

A template function works for any type that supports the operations used inside it:

template <typename T>
T GetMax(const T& var1, const T& var2) {
    return (var1 > var2) ? var1 : var2;
}

The function can be called explicitly (you specify T) or implicitly (the compiler deduces T from the arguments):

int maxVal = GetMax<int>(num1, num2);   // explicit
int maxVal = GetMax(num1, num2);         // implicit — compiler deduces T = int
Prefer implicit deduction when possible — it's less verbose and still fully type-safe. Use explicit instantiation when deduction would be ambiguous or when you need to force a specific type.

3

Template Classes

Template classes parameterize the entire class. You can have more than one template parameter:

template <typename T>
class Container {
    T value;
public:
    Container(T input) : value(input) {}
};

The type must be stated when instantiating a template class:

Container<int>  intContainer;
Container<bool> boolContainer;

Member function definitions outside the class also require the template prefix:

template <typename T>
void Container<T>::SomeFunc() {
    // ...
}
Templated classes that are defined but never instantiated are ignored by the compiler — no code is generated until you create an object with a specific type. This is why template definitions typically live in header files rather than .cpp files.

4

Template Specialization

Sometimes a template needs different behavior for a specific type. You can provide a specialization using template<> (empty parameter list):

// General template
template <typename T>
class Container {
    T value;
    // ...
};

// Specialization for int — completely different implementation
template<>
class Container<int> {
    int value;
    std::string extraField;  // added for int-specific behavior
    // ...
};
Specializations are selected automatically — Container<int> uses the specialization, everything else uses the general template.

5

Template Static Members

Static members in a template class are shared among all instances of the same type parameter. Different template instantiations each get their own separate static:

template <typename T>
class Counter {
public:
    static int count;  // shared among all Counter<T> of the SAME T
};

// Must be defined outside the class, with the template prefix
template <typename T>
int Counter<T>::count = 0;

// Counter<int>::count and Counter<double>::count are separate variables

6

Variadic Templates

Variadic templates accept a variable number of template arguments. The ... syntax represents a parameter pack:

// Base case — handles the last argument
template <typename T>
void printer(T p) {
    std::cout << p << std::endl;
}

// Recursive case — peels one argument off Rest each call
template <typename T1, typename... Rest>
void printer(T1 first, Rest... rest) {
    printer(first);      // print this argument
    printer(rest...);    // recurse with the remaining arguments
}

printer(1, 2, 3, 4, 5);  // works for any number of args

You can check how many arguments remain with sizeof...:

std::cout << sizeof...(Rest) << std::endl;  // number of args in Rest
In C++17, variadic templates can be simplified further using fold expressions, which eliminate the need for a recursive base case in many scenarios.

7

static_assert

static_assert is a compile-time assert — it halts compilation with a custom error message if a condition is false. It's especially useful for catching invalid template instantiations early:

// Prevent instantiation with int
static_assert(sizeof(T) != sizeof(int), "No ints please!");

// Ensure a type is not a pointer
static_assert(!std::is_pointer<T>::value, "T must not be a pointer type");
Unlike assert (runtime, debug-only), static_assert runs at compile time and is always active — there is no Release vs Debug distinction.

8

typename vs class

In template parameter lists, typename and class are identical:

template <class T>     // same as...
template <typename T>  // ...this

However, there are situations where you must use typename — when referring to a nested dependent type name. The compiler cannot tell if T::SomeType is a type or a static variable, so it assumes it's a variable. You must explicitly tell it otherwise:

// WITHOUT typename — compiler assumes T::const_iterator is a variable
template <typename T>
void Func(T container) {
    T::const_iterator x = container.cbegin();  // ERROR
}

// WITH typename — compiler knows it's a type
template <typename T>
void Func(T container) {
    typename T::const_iterator x = container.cbegin();  // correct
}
Always prefix nested dependent type names with typename. The rule: if a name depends on a template parameter AND refers to a type, it needs typename.

9

Generic Programming Constraints

Templates support interfaces and polymorphism — but the interfaces are implicit. A template function like this:

template <typename T>
void func(T val) {
    if (val.size() > 10 && val != 10) { // ... }
}

…requires that T supports .size() and operator!=. But this is only enforced when the template is instantiated — not when it's defined. If you pass a type that doesn't support those operations, you get a potentially cryptic compile error deep inside the template.

C++20 introduced Concepts to express these requirements explicitly and produce cleaner error messages: template <typename T> requires std::ranges::sized_range<T>.

10

Copy/Assignment with Templates

If you declare a templated copy constructor or assignment operator (e.g. to accept a Container<U> from a Container<T>), you must also declare the normal non-templated versions. Otherwise the compiler generates its own default ones, which may shadow your template:

template <typename T>
class Container {
public:
    // Generalized copy — converts Container<U> to Container<T>
    template <typename U>
    Container(const Container<U>& other);

    // Still need the normal copy constructor explicitly
    Container(const Container<T>& other);
};

11

std::tuple

std::tuple is a fixed-size collection of heterogeneous values. Requires #include <tuple>.

#include <tuple>

// Create a tuple with make_tuple
std::tuple<int, char, std::string> myTup = std::make_tuple(5, 'r', "hello");
OperationCodeResult
Sizestd::tuple_size<decltype(myTup)>::value3
Access by indexstd::get<1>(myTup)'r'
Concatenatestd::tuple_cat(tup1, tup2)Combined tuple
Unpackstd::tie(ignore, cVar, ignore) = myTup;Unpacks into variables

std::tie unpacks a tuple into separate variables. Use std::ignore to skip elements you don't need:

char cVar;
std::tie(std::ignore, cVar, std::ignore) = myTup;
std::cout << cVar;  // 'r'
In C++17, structured bindings are a cleaner alternative to std::tie:

auto [num, ch, str] = myTup;

This declares and initializes all three variables in one line.
← Chapter 17 ↑ Index