Templates and Generic Code

Templates and Generic Code

Write reusable functions and classes that work across multiple types.

Templates and Generic Code

A generic function

template <typename T>
T add(T a, T b) {
    return a + b;
}

Why templates are useful

One implementation can support many types without runtime overhead.

That is the main tradeoff: more work at compile time in exchange for zero-cost abstraction at runtime.

A generic class

template <typename T>
class Holder {
public:
    explicit Holder(T value) : value_{std::move(value)} {}
    const T& get() const { return value_; }

private:
    T value_;
};

This is useful when the type of the stored value should be chosen by the caller rather than fixed by the class author.

Larger worked example

template <typename T>
class RunningTotal {
public:
    void add(T value) {
        total_ += value;
        ++count_;
    }

    T total() const {
        return total_;
    }

    std::size_t count() const {
        return count_;
    }

private:
    T total_{};
    std::size_t count_{};
};

This is a realistic generic type: the data layout and operations depend on the value type, but the behavior stays the same.

Modern advice

Template argument deduction in practice

auto sum = add(2, 3);
auto weight = add(1.5, 2.25);

The compiler deduces T from the call arguments, so callers usually do not need to write the template argument explicitly.

Concepts make templates friendlier

template <typename T>
concept Summable = requires(T a, T b) {
    a + b;
};

template <Summable T>
T add_all(T a, T b) {
    return a + b;
}

Good constraints move errors toward the call site and describe the intent of the API.

template <std::integral T>
T twice(T value) {
    return value + value;
}

This is often easier to read than older enable_if-based styles.

Constraint failure example

template <std::integral T>
T increment(T value) {
    return value + 1;
}

auto result = increment(std::string{"Ada"});

The important design benefit is that the error points at the violated contract, not deep inside the implementation. Readers can see immediately that std::string is not an integral type.

Useful type traits

template <typename T>
void describe(T&& value) {
    using Raw = std::remove_cvref_t<T>;
    if constexpr (std::is_integral_v<Raw>) {
        std::cout << "integral\n";
    }
}

Design guidance

Start with the simplest generic function that works, then add constraints only when the interface becomes unclear or misuse is likely.

if constexpr in practice

template <typename T>
void print_value(const T& value) {
    if constexpr (std::integral<T>) {
        std::cout << "integral: " << value << '\n';
    } else {
        std::cout << "value: " << value << '\n';
    }
}

This keeps one generic implementation readable when only a small part of the behavior depends on the type.

When templates are the right tool

When not to use templates

Practical rule of thumb

Example in practice

template <typename T>
T twice(T value) {
    return value + value;
}

int main() {
    return twice(21);
}

Try this variation

Add a concept or `static_assert` so the template fails early for the wrong type. That is usually the first quality jump in generic code.

#include <concepts>

template <std::integral T>
T twice(T value) {
    return value + value;
}