A template describes a family of functions or types. std::vector<int> and std::vector<std::string> use the same class template with different element types. The compiler checks the operations required by the particular specialization you use; there is no requirement to convert every element to a common runtime base class.
Generic code is useful when the algorithm remains the same and only the supported types vary. Start with a working concrete implementation, identify the actual type requirements, then generalize. Making every function a template before a second use appears can make an otherwise simple interface harder to understand.
Express requirements with concepts
A concept is a named compile-time constraint. The standard <concepts> header includes constraints such as std::integral, std::floating_point and std::same_as. Constraints help exclude unsuitable overloads and produce diagnostics closer to the caller's mistake.
The std::integral concept includes bool and character types. If a reusable integer interface should exclude bool, say so explicitly. A concept describes properties of a type or expressions involving it; it does not inspect every runtime value. A positive-duration parameter still needs a value check even when its type is integral.
Save as templates.cpp:
#include <concepts>
#include <exception>
#include <iostream>
#include <stdexcept>
template<class T>
concept Integer = std::integral<T> && (!std::same_as<T, bool>);
template<Integer T>
constexpr T larger(T left, T right) {
return left < right ? right : left;
}
template<Integer T>
class ClosedRange {
public:
constexpr ClosedRange(T low, T high) : low_(low), high_(high) {
if (high < low) {
throw std::invalid_argument("Range is reversed");
}
}
constexpr bool contains(T value) const {
return value >= low_ && value <= high_;
}
private:
T low_;
T high_;
};
static_assert(Integer<int>);
static_assert(!Integer<bool>);
static_assert(larger(4, 9) == 9);
constexpr ClosedRange<int> valid_scores{0, 100};
static_assert(valid_scores.contains(60));
static_assert(!valid_scores.contains(101));
int main() {
try {
const ClosedRange<long long> minutes{1, 1440};
std::cout << larger(7, 3) << '\n';
std::cout << std::boolalpha << minutes.contains(90) << '\n';
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
clang++ -std=c++20 -Wall -Wextra -Wpedantic -g templates.cpp -o templates
./templates
The program prints 7 and true on separate lines. The static_assert checks run during compilation and generate no runtime test output. The range constructor rejects reversed endpoints; valid inputs can also construct a range in a constant expression.
Deduction, instantiation and visibility
In larger(7, 3), template argument deduction determines that T is int from both arguments. larger(7, 3LL) does not deduce one consistent T; explicitly converting both values to an intended type or writing larger<long long>(7, 3LL) resolves that mismatch. Such conversions still deserve a range check when the source values might not fit.
Instantiation produces the needed specialization from the template definition. Implementations can share generated code, so avoid claiming that every specialization always creates a separate machine-code copy. Different type combinations can nevertheless increase compilation work and executable size.
Ordinary template definitions usually belong in headers so every translation unit that needs to instantiate them can see the implementation. A declaration in a header with only an ordinary definition hidden in a source file is insufficient for arbitrary type arguments. Explicit instantiation can support a deliberately limited set of types, but that is an additional design technique.
constexpr allows eligible calls to participate in constant evaluation; it does not force every call to happen during compilation. In a required constant expression, a path that throws cannot produce a constant result. At runtime, the same invalid range construction throws normally.
Contracts concepts cannot prove
A comparison concept can check that comparison expressions exist, but it cannot generally prove their mathematical consistency. For example, floating-point NaN values complicate ordering even though floating types satisfy common syntactic comparison constraints. This lesson deliberately uses integral values and performs no arithmetic inside its generic comparison.
Likewise, adding std::integral to a multiplication function would not prevent signed overflow. Concepts constrain supported operations; runtime preconditions and numeric analysis establish whether particular inputs are safe. Keep diagnostic messages focused on the first failed requirement instead of trying to interpret the whole compiler trace at once.
Practice and solution guidance
- Add a
clampoperation toClosedRange. Guidance: return the low endpoint below the range, the high endpoint above it, and the input otherwise. Test both endpoints without subtracting values. - Intentionally call
largerwith strings or booleans. Guidance: observe a constraint failure; remove the invalid call before running the complete example again. - Move the template into a guarded header and use it from two source files. Guidance: keep its full definition visible and build both callers together.

