Skip to content

2. Variadic & Advanced Templates

What & why

Templates are a compile-time language of their own. This lesson covers the tools library authors use daily: parameter packs for any number of arguments, fold expressions to process them, type traits to inspect and transform types, SFINAE (the pre-C++20 way to constrain templates — you'll still read it everywhere), and CRTP for static polymorphism.

Variadic templates

A parameter pack accepts zero or more template arguments:

#include <iostream>

template <typename... Ts>           // template parameter pack
void print_all(const Ts&... args) { // function parameter pack
    std::cout << sizeof...(args) << " args\n";
}

print_all(1, "two", 3.0);   // Ts = int, char[4], double

Expanding packs

A pattern followed by ... is repeated for each element:

template <typename... Ts>
void forward_all(Ts&&... args) {
    target(std::forward<Ts>(args)...);        // target(std::forward<T1>(a1), std::forward<T2>(a2), ...)
}

template <typename... Ts>
auto sizes() {
    return std::array{sizeof(Ts)...};         // {sizeof(T1), sizeof(T2), ...}
}

Recursion (the pre-C++17 way)

void print() {}                                // base case

template <typename T, typename... Rest>
void print(const T& first, const Rest&... rest) {
    std::cout << first << ' ';
    print(rest...);                            // recurse on the tail
}

Fold expressions (C++17)

Apply a binary operator across a pack — no recursion needed:

template <typename... Ts>
auto sum(const Ts&... xs) {
    return (xs + ... + 0);                     // binary right fold with init 0
}

template <typename... Ts>
bool all_true(const Ts&... xs) {
    return (... && xs);                        // unary left fold; empty pack → true
}

template <typename... Ts>
void print_line(const Ts&... xs) {
    ((std::cout << xs << ' '), ...);           // fold over the comma operator
    std::cout << '\n';
}
Form Expands to
(pack op ...) x1 op (x2 op (x3 op x4))
(... op pack) ((x1 op x2) op x3) op x4
(pack op ... op init) x1 op (x2 op (x3 op init))
(init op ... op pack) ((init op x1) op x2) op x3

Empty packs are allowed only for && (→ true), || (→ false), and , (→ void()).

Type traits

<type_traits> provides compile-time queries and transformations:

#include <type_traits>

// Queries (value is a bool constant)
std::is_integral_v<int>                     // true
std::is_same_v<int, long>                   // false
std::is_base_of_v<Shape, Circle>            // true
std::is_trivially_copyable_v<Point>         // true for simple structs
std::is_nothrow_move_constructible_v<T>

// Transformations (produce a type)
std::remove_cvref_t<const int&>             // int
std::decay_t<int[3]>                        // int*
std::add_pointer_t<int>                     // int*
std::conditional_t<(sizeof(void*) == 8), std::int64_t, std::int32_t>
std::common_type_t<int, double>             // double
std::underlying_type_t<MyEnum>

Writing your own trait

template <typename T>
struct is_vector : std::false_type {};

template <typename T, typename A>
struct is_vector<std::vector<T, A>> : std::true_type {};   // partial specialization

template <typename T>
inline constexpr bool is_vector_v = is_vector<T>::value;

static_assert(is_vector_v<std::vector<int>>);
static_assert(!is_vector_v<std::array<int, 3>>);

decltype and std::declval

decltype(expr) gives the type of an expression without evaluating it. std::declval<T>() pretends to produce a T in unevaluated contexts:

template <typename A, typename B>
using product_t = decltype(std::declval<A>() * std::declval<B>());

static_assert(std::is_same_v<product_t<int, double>, double>);

SFINAE

"Substitution Failure Is Not An Error": if substituting template arguments into a declaration produces an invalid type, that overload is silently removed from consideration instead of causing an error. Before C++20, this was the way to constrain templates:

// Only enabled for integral types
template <typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
T half(T x) { return x / 2; }

// Only enabled for floating-point types
template <typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
T half(T x) { return x * 0.5; }

Detecting whether a type has a member, the classic way:

template <typename T, typename = void>
struct has_size : std::false_type {};

template <typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>> : std::true_type {};

In C++20 all of this is dramatically simpler with concepts (next lesson):

template <typename T>
concept has_size = requires(T t) { t.size(); };

Learn to read SFINAE; write concepts.

Tag dispatch

Select an implementation by overloading on a "tag" type:

template <typename It>
void advance_impl(It& it, int n, std::random_access_iterator_tag) {
    it += n;                                   // O(1)
}

template <typename It>
void advance_impl(It& it, int n, std::input_iterator_tag) {
    while (n-- > 0) ++it;                      // O(n)
}

template <typename It>
void my_advance(It& it, int n) {
    advance_impl(it, n, typename std::iterator_traits<It>::iterator_category{});
}

Today, if constexpr or concepts usually express this more directly.

CRTP: the Curiously Recurring Template Pattern

A class derives from a template instantiated with itself. The base can call into the derived class at compile time — polymorphism without virtual calls:

template <typename Derived>
class Shape {
public:
    double area() const {
        return static_cast<const Derived&>(*this).area_impl();
    }
    void describe() const {
        std::cout << "area = " << area() << '\n';
    }
};

class Square : public Shape<Square> {
public:
    explicit Square(double s) : s_{s} {}
    double area_impl() const { return s_ * s_; }
private:
    double s_;
};

template <typename D>
void print_area(const Shape<D>& s) { s.describe(); }   // resolved statically, inlinable

Common uses: mixins that add behavior (Comparable<Derived> generating operators from one function, Counter<Derived> counting instances per type), and static interfaces in performance-critical code. C++23's deducing this replaces many CRTP uses:

struct Shape {
    void describe(this const auto& self) {        // `self` has the derived type
        std::cout << "area = " << self.area() << '\n';
    }
};

Variable templates and alias templates

template <typename T>
inline constexpr T pi_v = T(3.14159265358979323846L);

float f = pi_v<float>;

template <typename T>
using Vec = std::vector<T, MyAllocator<T>>;

std::tuple and index sequences

Iterate a tuple at compile time with std::apply:

#include <tuple>

std::tuple<int, std::string, double> t{1, "two", 3.0};

std::apply([](const auto&... xs) {
    ((std::cout << xs << ' '), ...);
}, t);

Or with an index sequence when you need the positions:

template <typename Tuple, std::size_t... Is>
void print_indexed(const Tuple& t, std::index_sequence<Is...>) {
    ((std::cout << Is << ": " << std::get<Is>(t) << '\n'), ...);
}

template <typename... Ts>
void print_indexed(const std::tuple<Ts...>& t) {
    print_indexed(t, std::index_sequence_for<Ts...>{});
}

Common mistakes

  • Forgetting ... after a pattern — "parameter packs not expanded".
  • Using std::move instead of std::forward in pack expansions.
  • Empty-pack folds with operators other than &&, ||, , (compile error) — provide an init value.
  • SFINAE only on the return type or body — errors in the body are hard errors, not substitution failures.
  • CRTP typos — class B : public Shape<A> compiles and does the wrong thing. Add a private constructor with friend Derived; in the base to catch it.
  • Over-engineering — metaprogramming nobody can maintain. Prefer if constexpr and concepts.

Exercises

  1. Write print_line(args...) with a fold that prints a comma-separated list with no trailing comma. (Hint: fold over a lambda that tracks whether it's the first element.)
  2. Write constexpr bool contains_type<T, Ts...> using a fold over std::is_same_v.
  3. Write min_of(args...) returning the smallest argument, requiring at least one argument.
  4. Implement has_size via SFINAE and via a concept; write a print_size(const T&) that uses each.
  5. Implement a CRTP Comparable<Derived> that provides ==, !=, <, >, <=, >= given only Derived::compare(const Derived&) const -> int. Then do it again with <=> and explain why CRTP is less needed now.
  6. Write for_each_in_tuple(tuple, fn) with std::apply and then with an index_sequence.

Recap

  • Parameter packs (typename... Ts) + expansion (pattern...) handle any number of arguments.
  • Fold expressions ((... op xs)) reduce packs without recursion.
  • <type_traits> inspects and transforms types; write your own with partial specialization.
  • SFINAE removes invalid overloads silently — read it, but use concepts in new code.
  • CRTP gives static polymorphism and mixins; C++23 deducing this covers many of its uses.
  • std::apply and std::index_sequence iterate tuples at compile time.

Next: Concepts →