Back to blog
Backend Systemsbeginner

C++ Templates & Modern C++ (C++20)

Write generic, reusable C++ with templates. Learn function templates, class templates, concepts, and key modern C++ features: auto, constexpr, lambdas, and structured bindings.

Asma HafeezApril 17, 20265 min read
cpptemplatesc++20genericsmodern-cpp
Share:𝕏

C++ Templates & Modern C++

Templates are C++'s mechanism for generic programming. Modern C++ (C++11-20) added features that make the language dramatically more expressive.


Function Templates

CPP
// Generic max function — works for any comparable type
template<typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

std::cout << max(3, 7);           // 7 (T = int)
std::cout << max(3.14, 2.71);     // 3.14 (T = double)
std::cout << max('a', 'z');       // z (T = char)

// Multiple type parameters
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
    return a + b;
}

auto result = add(3, 4.5);  // double: 7.5

Class Templates

CPP
template<typename T>
class Stack {
    std::vector<T> data_;

public:
    void push(const T& value) {
        data_.push_back(value);
    }

    T pop() {
        if (empty()) throw std::underflow_error("Stack is empty");
        T top = data_.back();
        data_.pop_back();
        return top;
    }

    const T& top() const {
        if (empty()) throw std::underflow_error("Stack is empty");
        return data_.back();
    }

    bool empty() const { return data_.empty(); }
    size_t size() const { return data_.size(); }
};

Stack<int> intStack;
intStack.push(1);
intStack.push(2);
intStack.push(3);
std::cout << intStack.pop();  // 3
std::cout << intStack.top();  // 2

Stack<std::string> strStack;
strStack.push("hello");
strStack.push("world");

Concepts (C++20) — Constrained Templates

CPP
#include <concepts>

// Require T to support +, -, *, /
template<typename T>
concept Numeric = std::is_arithmetic_v<T>;

template<Numeric T>
T average(const std::vector<T>& values) {
    if (values.empty()) return T{};
    T sum = T{};
    for (const T& v : values) sum += v;
    return sum / static_cast<T>(values.size());
}

std::cout << average(std::vector<int>{1, 2, 3, 4, 5});     // 3
std::cout << average(std::vector<double>{1.5, 2.5, 3.0});  // 2.333...

// average(std::vector<std::string>{"a","b"});  // COMPILE ERROR — not Numeric

auto — Type Inference

CPP
auto x = 42;               // int
auto d = 3.14;             // double
auto s = std::string("hi"); // std::string

// In range-for
std::vector<std::string> names = {"Alice", "Bob"};
for (const auto& name : names) {  // auto& avoids copy
    std::cout << name << "\n";
}

// Auto for complex types
auto it = names.begin();  // std::vector<std::string>::iterator

// Return type deduction (C++14)
auto multiply(int a, int b) {
    return a * b;  // deduced as int
}

Lambda Expressions

CPP
// Basic lambda: [capture](params) -> return_type { body }
auto greet = [](const std::string& name) {
    std::cout << "Hello, " << name << "!\n";
};
greet("Alice");

// Capture local variables
int factor = 3;
auto triple = [factor](int x) { return x * factor; };  // capture by value
auto scale  = [&factor](int x) { return x * factor; }; // capture by reference

// Used with algorithms
std::vector<int> v = {5, 2, 8, 1, 9, 3};
std::sort(v.begin(), v.end(), [](int a, int b){ return a < b; });

// Generic lambda (C++14)
auto print_all = [](const auto& container) {
    for (const auto& item : container) std::cout << item << " ";
    std::cout << "\n";
};
print_all(v);
print_all(std::vector<std::string>{"a", "b", "c"});

constexpr — Compile-Time Computation

CPP
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

constexpr int f5 = factorial(5);  // computed at compile time: 120

// constexpr class
class Point {
    double x_, y_;
public:
    constexpr Point(double x, double y) : x_(x), y_(y) {}
    constexpr double x() const { return x_; }
    constexpr double y() const { return y_; }
    constexpr double distanceFromOrigin() const {
        return std::sqrt(x_*x_ + y_*y_);  // compile-time if input is constexpr
    }
};

constexpr Point p{3.0, 4.0};
constexpr double dist = p.distanceFromOrigin();  // 5.0 at compile time

Structured Bindings (C++17)

CPP
// Decompose pairs and tuples
auto [min_val, max_val] = std::minmax({1, 2, 3, 4, 5});

// Decompose map entries
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
for (const auto& [name, score] : scores) {
    std::cout << name << ": " << score << "\n";
}

// Return multiple values from a function
std::tuple<int, double, std::string> getInfo() {
    return {42, 3.14, "hello"};
}

auto [n, d, s] = getInfo();
std::cout << n << " " << d << " " << s << "\n";

std::optional (C++17)

CPP
#include <optional>

std::optional<int> divide(int a, int b) {
    if (b == 0) return std::nullopt;  // no value
    return a / b;
}

auto result = divide(10, 2);
if (result) {
    std::cout << *result << "\n";  // 5
}

// With default
int val = divide(10, 0).value_or(-1);  // -1

std::variant (C++17)

CPP
#include <variant>

using Value = std::variant<int, double, std::string>;

Value v = 42;
v = 3.14;
v = "hello";

std::visit([](auto&& val) {
    std::cout << val << "\n";
}, v);

// Type check
if (std::holds_alternative<std::string>(v)) {
    std::cout << "It's a string: " << std::get<std::string>(v) << "\n";
}

Modern C++ Best Practices

CPP
// 1. Use nullptr, not NULL or 0
int* p = nullptr;

// 2. Range-for over index loops
for (const auto& item : container) { }

// 3. Uniform initialization
std::vector<int> v{1, 2, 3};  // not v = {1,2,3} or v(1,2,3)
int x{42};

// 4. Use emplace_back over push_back for complex types
struct Point { int x, y; };
std::vector<Point> points;
points.emplace_back(1, 2);  // constructs in-place, no copy

// 5. [[nodiscard]] — force callers to check return values
[[nodiscard]] bool connect(const std::string& host);
// connect("localhost");  // warning: ignoring return value

// 6. if constexpr — compile-time branching
template<typename T>
void process(T value) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << "Integer: " << value << "\n";
    } else if constexpr (std::is_floating_point_v<T>) {
        std::cout << "Float: " << value << "\n";
    }
}

Key Takeaways

  1. Templates are resolved at compile time — each instantiation generates specialized code
  2. Concepts (C++20) make template errors readable and self-documenting
  3. auto reduces verbosity without sacrificing type safety
  4. Lambdas are the idiomatic way to pass behavior to algorithms
  5. std::optional, std::variant, structured bindings — use these instead of output parameters and null checks

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.