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):
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::moveinstead ofstd::forwardin 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 withfriend Derived;in the base to catch it. - Over-engineering — metaprogramming nobody can maintain. Prefer
if constexprand concepts.
Exercises¶
- 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.) - Write
constexpr bool contains_type<T, Ts...>using a fold overstd::is_same_v. - Write
min_of(args...)returning the smallest argument, requiring at least one argument. - Implement
has_sizevia SFINAE and via a concept; write aprint_size(const T&)that uses each. - Implement a CRTP
Comparable<Derived>that provides==,!=,<,>,<=,>=given onlyDerived::compare(const Derived&) const -> int. Then do it again with<=>and explain why CRTP is less needed now. - Write
for_each_in_tuple(tuple, fn)withstd::applyand then with anindex_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
thiscovers many of its uses. std::applyandstd::index_sequenceiterate tuples at compile time.
Next: Concepts →