Modern C++ Fundamentals: From Basics to Smart Pointers and Threads · Ownership, Templates, Errors and Concurrency
Templates and Concepts
Write function and class templates, constrain them with C++20 concepts and requires clauses, and read the clear error messages that constraints give you.
What you'll build
A generic average function that accepts only numeric types, a Matrix<T> class template, and a Printable concept that checks that a type can be streamed to std::cout.
The idea
Templates let you write code once and use it with many types, with no runtime cost: the compiler generates a specialized version for each type you use. The whole standard library is templates (std::vector<T>, std::sort).
template <typename T>
T largest(T a, T b) { return a > b ? a : b; }
Before C++20, a template accepted any type and failed with pages of cryptic errors deep inside the function when a type didn't fit. Concepts (C++20) let you state the requirements:
- Use ready-made ones from
<concepts>:std::integral,std::floating_point,std::same_as<T>,std::totally_ordered. - Constrain a parameter:
template <std::integral T>, or writerequires std::integral<T>. - Build your own:
template <typename T> concept Printable = requires(T value) { std::cout << value; };Arequiresexpression lists operations that must compile. - Abbreviated syntax:
void show(const Printable auto& value).
Class templates work the same way: template <typename T> class Matrix { ... };. Template definitions normally live in headers, because the compiler needs to see them to instantiate them.
The code
#include <concepts>
#include <iostream>
#include <string>
#include <vector>
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template <typename T>
concept Printable = requires(T value) { std::cout << value; };
// Accepts any numeric type, and returns a double
template <Numeric T>
double average(const std::vector<T>& values) {
if (values.empty()) return 0.0;
double sum = 0;
for (T v : values) sum += static_cast<double>(v);
return sum / static_cast<double>(values.size());
}
// Abbreviated constrained template
void show(const Printable auto& value) { std::cout << "[" << value << "]\n"; }
template <typename T>
class Matrix {
public:
Matrix(std::size_t rows, std::size_t cols, T fill = T{}) : rows_{rows}, cols_{cols}, cells_(rows * cols, fill) {}
T& at(std::size_t r, std::size_t c) { return cells_.at(r * cols_ + c); }
const T& at(std::size_t r, std::size_t c) const { return cells_.at(r * cols_ + c); }
Matrix transposed() const {
Matrix result{cols_, rows_};
for (std::size_t r = 0; r < rows_; ++r)
for (std::size_t c = 0; c < cols_; ++c) result.at(c, r) = at(r, c);
return result;
}
void print() const {
for (std::size_t r = 0; r < rows_; ++r) {
for (std::size_t c = 0; c < cols_; ++c) std::cout << at(r, c) << ' ';
std::cout << '\n';
}
}
private:
std::size_t rows_, cols_;
std::vector<T> cells_;
};
int main() {
std::cout << average(std::vector<int>{1, 2, 3, 4}) << '\n';
std::cout << average(std::vector<double>{1.5, 2.5}) << '\n';
// average(std::vector<std::string>{"a"}); // error: constraints not satisfied (std::string is not Numeric)
show(42);
show(std::string{"text"});
Matrix<int> m{2, 3};
int counter = 1;
for (std::size_t r = 0; r < 2; ++r)
for (std::size_t c = 0; c < 3; ++c) m.at(r, c) = counter++;
m.print();
std::cout << "transposed:\n";
m.transposed().print();
}
Output
2.5 2 [42] [text] 1 2 3 4 5 6 transposed: 1 4 2 5 3 6
Run this on your computer — see “Get Started” for setup.
Uncomment the average(std::vector<std::string>{"a"}) line and the compiler reports constraints not satisfied with the name of the failed concept, not a wall of template internals.
Common mistakes
- Putting template definitions in a
.cppfile. The compiler needs them in headers to instantiate for each type. - Writing unconstrained templates for public APIs. Add a concept so misuse produces a short, clear error.
- Over-using templates. If a function takes only
int, a plain function is simpler. - Forgetting
typenamewhen naming a dependent type, such astypename std::vector<T>::iterator. - Assuming a template compiles for all types. It's only checked when instantiated; test it with every type you intend to support.
Key points
- Templates generate type-specific code at compile time with no runtime overhead.
- Constrain templates with concepts such as
std::integralor your ownrequiresexpressions. - Class templates (
Matrix<T>) work like function templates; define them in headers. - Concepts turn confusing template errors into clear "constraint not satisfied" messages.
Further reading: cppreference: Constraints and concepts.
Practice questions
What problem do concepts solve compared to unconstrained templates?
Answer
Unconstrained templates accept any type and fail with long, confusing errors inside the template body. Concepts state the requirements in the signature, so unsuitable types are rejected at the call site with a clear message.
Why are template definitions usually placed in header files?
Answer
The compiler must see the full definition to generate (instantiate) a version for each type used. If the definition is only in a .cpp file, other files cannot instantiate it and linking fails.
Open in the app to mark lessons complete, take notes and track your progress.