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
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.5Class 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 Numericauto — 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 timeStructured 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); // -1std::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
- Templates are resolved at compile time — each instantiation generates specialized code
- Concepts (C++20) make template errors readable and self-documenting
autoreduces verbosity without sacrificing type safety- Lambdas are the idiomatic way to pass behavior to algorithms
std::optional,std::variant, structured bindings — use these instead of output parameters andnullchecks
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.