Templates and Generic Code
Templates and Generic Code
Write reusable functions and classes that work across multiple types.
Templates and Generic Code
Write reusable functions and classes that work across multiple types.
template <typename T>
T add(T a, T b) {
return a + b;
}
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.
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.
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.
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.
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.
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.
std::is_integral_v<T>std::is_same_v<T, U>std::remove_cvref_t<T>std::is_invocable_v<F, Args...>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";
}
}
Start with the simplest generic function that works, then add constraints only when the interface becomes unclear or misuse is likely.
if constexpr in practicetemplate <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.
template <typename T>
T twice(T value) {
return value + value;
}
int main() {
return twice(21);
}
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;
}