C++ Function and Class Templates: Build One Type-Safe Range Pattern
Build a reusable C++20 clamp function and Range class that work for int and double while catching mixed-type calls during compilation.
KnowledgeGate Team
Exam prep & CS education

Writing maxInt, maxDouble, IntRange and DoubleRange repeats the same logic under different type names. Replacing every type with auto does not create a reusable, type-checked abstraction. A function template clamps a value, and a class template owns valid bounds, so the same source handles integer marks and double temperatures while rejecting a mixed-type call during compilation. The pattern builds on functions, classes and constructors in Coding & DSA Courses for Placements. The broader Templates in C++: Function and Class Templates with Runnable Examples adds non-type parameters and specialisation, while the Range<T> and clamp_value<T> pair below isolates same-type deduction, bounds invariants and mixed-type rejection.
1. C++ templates are compile-time patterns with type parameters
A template is a source-code pattern parameterised by a type. template <typename T> introduces the type parameter T. The compiler substitutes a concrete type when the program uses the template, not when it creates an object at runtime.
A function template can generate functions such as clamp_value<int> and clamp_value<double>. A class template can generate types such as Range<int> and Range<double>. Runtime object creation happens later, after those concrete types have been formed.
Our safety rule is precise. All three clamp_value arguments must deduce the same T, and every Range<T> stores both bounds as that one type. Therefore, clamp_value(127, 0, 100) deduces int, while clamp_value(16.8, 18.5, 24.0) deduces double.
Templates are not macros. The compiler parses and type-checks the pattern. If deduction or a constraint fails, compilation stops instead of performing untyped text replacement.
2. Function template worked example: clamp one value into typed bounds
The pattern relies on the C++20 standard concepts std::totally_ordered and std::same_as. The function is:
#include <concepts>
#include <stdexcept>
template <std::totally_ordered T>
T clamp_value(T value, T low, T high) {
if (high < low) {
throw std::invalid_argument("low must not exceed high");
}
if (value < low) return low;
if (high < value) return high;
return value;
}std::totally_ordered states the ordering ability expected from T. The shared parameter type is what blocks conflicting deduction. Now trace every branch:
clamp_value(127, 0, 100)returns100because100 < 127is true.clamp_value(-8, 0, 100)returns0because-8 < 0is true.clamp_value(72, 0, 100)returns72because it lies inside the bounds.clamp_value(16.8, 18.5, 24.0)returns18.5because16.8 < 18.5is true.
By contrast, clamp_value(72, 0.0, 100.0) does not compile. The first argument tries to deduce T as int, while the bounds try to deduce it as double. This is a compile-time deduction failure, not a runtime exception.

3. Class template: make valid bounds a reusable type
The free function performs one operation. The class template is the stateful half of the pattern: it preserves valid bounds and offers behaviour that uses them. This separation between invariant and behaviour is also useful when studying Object Oriented Technology Explained.
template <std::totally_ordered T>
class Range {
T low_;
T high_;
public:
Range(T low, T high) : low_{low}, high_{high} {
if (high_ < low_) {
throw std::invalid_argument("low must not exceed high");
}
}
template <typename U>
requires std::same_as<U, T>
bool contains(U value) const {
return !(value < low_) && !(high_ < value);
}
template <typename U>
requires std::same_as<U, T>
T clamp(U value) const {
return clamp_value(value, low_, high_);
}
};After construction, the invariant is low_ is not greater than high_. Range<int> reversed{100, 0} has valid types but invalid values, so its constructor throws std::invalid_argument at runtime.
The member templates solve a different problem. Their U must be exactly T, so Range<int>::clamp(72.5) fails its compile-time constraint instead of silently narrowing a double to an int.
4. One pattern, two concrete types: run the complete worked program
The program uses class template argument deduction. Class template argument deduction uses the constructor arguments to form Range<int> and Range<double>.
#include <concepts>
#include <iostream>
#include <stdexcept>
template <std::totally_ordered T>
T clamp_value(T value, T low, T high) {
if (high < low) {
throw std::invalid_argument("low must not exceed high");
}
if (value < low) return low;
if (high < value) return high;
return value;
}
template <std::totally_ordered T>
class Range {
T low_;
T high_;
public:
Range(T low, T high) : low_{low}, high_{high} {
if (high_ < low_) {
throw std::invalid_argument("low must not exceed high");
}
}
template <typename U>
requires std::same_as<U, T>
bool contains(U value) const {
return !(value < low_) && !(high_ < value);
}
template <typename U>
requires std::same_as<U, T>
T clamp(U value) const {
return clamp_value(value, low_, high_);
}
};
int main() {
Range marks{0, 100};
Range room_temperature{18.5, 24.0};
std::cout << std::boolalpha;
std::cout << marks.clamp(127) << '\n';
std::cout << marks.contains(72) << '\n';
std::cout << room_temperature.clamp(16.8) << '\n';
std::cout << room_temperature.contains(22.6) << '\n';
}The exact output is:
100
true
18.5
trueFor marks.contains(72), both 72 < 0 and 100 < 72 are false. Negating both checks makes the result true. For room_temperature.clamp(16.8), the comparison 16.8 < 18.5 is true, so the function returns the lower bound 18.5.

5. Deduction, explicit arguments and instantiation: what the compiler actually does
The process has three useful stages:
Deduction selects
Tfrom function-call or constructor arguments.Constraint checking confirms that the selected type supports the promised operations.
Instantiation forms the concrete function or class required by the program.
This program uses clamp_value<int>, clamp_value<double>, Range<int> and Range<double>. One checked source pattern produces the exact specialisations that are used.
An explicit argument changes deduction. clamp_value<double>(72, 0.0, 100.0) is valid and returns 72.0 because choosing T=double deliberately allows 72 to convert. Templates do not forbid every implicit conversion. They make the chosen type and its checks visible to the compiler.
Other languages provide generic facilities with different syntax and rules. The Programming Languages Survey is a broader comparison route, but this example stays with C++ template behaviour.
6. Function and class template mistakes, why they fail and the repair
Tempting move | What goes wrong | Repair |
|---|---|---|
Call | Deduction conflicts between | Use one argument type, or explicitly choose |
Omit | The compiler sees an ordinary declaration with an unknown type | Put the template header immediately before the function or class |
Define a class-template member outside the class without its template header | The definition is no longer tied correctly to the class template | Repeat the class and member template headers with the qualified name |
Treat |
| Declare an object separately, such as |
Supply a type without the required ordering operations |
| Implement the required comparisons or use a suitable type |
Confuse bounds | Deduction succeeds, then the runtime invariant fails | Validate bound values in the constructor |
Keep template definitions visible to any translation unit that instantiates them, commonly by placing them in a header. Otherwise, the linker may report an unresolved specialisation. Also avoid Range<int>{0.5, 100.5}, whose braced initialisation attempts narrowing. Express the intended type directly as Range<double>{0.5, 100.5}.
Separate specialisations can increase generated code size. Templates remove repeated source logic, but they do not guarantee one shared machine-code body, a smaller binary or safe operations beyond the constraints and checks actually written.
7. How interviews and exam-style questions test C++ templates
Representative questions ask you to predict which calls compile, infer a deduced type, distinguish function and class templates, trace an instantiated call, find a missing template header, or explain a failed constraint. These are question forms, not claims about any current syllabus or paper pattern.
Try three answer-first checks:
clamp_value(5, 1, 9)deducesintand returns5.clamp_value(5, 1.0, 9.0)fails deduction becauseTconflicts.Range limits{2.5, 7.5}deducesRange<double>, solimits.clamp(9.0)returns7.5.
8. C++ function and class templates: the short version and next step
A function template generates functions.
A class template generates types.
Deduction and constraint checks happen at compile time.
Object invariants such as
low <= highstill depend on runtime values.
Rebuild the example from memory. Write clamp_value<T>, place the bounds in Range<T>, then verify the four outputs: 100, true, 18.5, true. If clamp_value(72, 0.0, 100.0) compiles in your version, check whether you explicitly forced a type or changed the parameter design.
The C++ Programming Course is the next structured step for C++ concepts, MCQs and coding questions.
Keep learning
Templates in C++: Reusable Parameterized Code with Worked Examples
Learn how one C++ template produces type-specific functions and classes. Follow exact calls, calculations, deduction failures, and practical corrections.
Friend Functions in C++: Controlled Class Access with a Worked Example
See how a non-member friend function can validate and update two Wallet objects while private balances stay hidden from ordinary callers. Includes runnable C++ and an exact trace.
Data Types in C++: Fundamental and Derived Types with Worked Examples
Learn how C++ fundamental types differ from arrays, pointers, references, and function types. Trace one complete program, then test the conversion traps.
C++ Constructors and Object Initialization: Techniques, Order and a Worked Trace
Learn how C++ chooses default, parameterised, delegating and copy constructors, then trace member states and initialization order through complete code.





